mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-17 14:04:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547b8abc56 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 |
@@ -1,10 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Ask for help on Discord before reporting a bug
|
||||
about: Follow the troubleshooting guide before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||
|
||||
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
@@ -26,14 +28,21 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||
- Do you have any gamepads connected to your host PC directly?
|
||||
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||
|
||||
**Client PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- Moonlight Version: [e.g. v0.9.0]
|
||||
- GPU: [e.g. Intel HD Graphics 520]
|
||||
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||
|
||||
**Moonlight Logs (please attach)**
|
||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||
|
||||
@@ -13,3 +13,7 @@
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
[submodule "libs"]
|
||||
path = libs
|
||||
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
|
||||
shallow = true
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
@@ -15,7 +16,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 7.1 surround sound audio support
|
||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||
- Direct touchscreen mouse control
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Gamepad support with force feedback for up to 4 players
|
||||
@@ -28,6 +29,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||
|
||||
#### Special Thanks
|
||||
|
||||
[](https://cloudsmith.com)
|
||||
|
||||
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||
|
||||
## Building
|
||||
|
||||
### Windows Build Requirements
|
||||
@@ -49,7 +56,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
* GCC or Clang
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* RedHat/Fedora: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
|
||||
### Steam Link Build Requirements
|
||||
@@ -68,6 +75,8 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: f0ae9dea50...376c576317
+21
-2
@@ -131,9 +131,11 @@ SOURCES += \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
settings/mappingfetcher.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input/abstouch.cpp \
|
||||
streaming/input/clipboard.cpp \
|
||||
streaming/input/gamepad.cpp \
|
||||
streaming/input/input.cpp \
|
||||
streaming/input/keyboard.cpp \
|
||||
@@ -155,6 +157,7 @@ SOURCES += \
|
||||
|
||||
HEADERS += \
|
||||
backend/nvapp.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
@@ -250,6 +253,12 @@ libdrm {
|
||||
DEFINES += HAVE_DRM
|
||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
@@ -264,7 +273,7 @@ config_EGL {
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
|
||||
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
LIBS += -lSLVideo -lSLAudio
|
||||
|
||||
SOURCES += \
|
||||
@@ -307,13 +316,23 @@ discord-rpc {
|
||||
LIBS += -ldiscord-rpc
|
||||
DEFINES += HAVE_DISCORD
|
||||
}
|
||||
embedded {
|
||||
message(Embedded build)
|
||||
|
||||
DEFINES += EMBEDDED_BUILD
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_fr.ts
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
languages/qml_ru.ts \
|
||||
languages/qml_es.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||
@@ -63,11 +63,41 @@ QString AutoUpdateChecker::getPlatform()
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||
// to be consistent (and not require another entry in the manifest).
|
||||
return QStringLiteral("osx");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||
for (int i = 0;; i++) {
|
||||
int v1Val = 0;
|
||||
int v2Val = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < version1.count()) {
|
||||
v1Val = version1[i];
|
||||
}
|
||||
if (i < version2.count()) {
|
||||
v2Val = version2[i];
|
||||
}
|
||||
if (i >= version1.count() && i >= version2.count()) {
|
||||
// Equal versions
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (v1Val < v2Val) {
|
||||
return -1;
|
||||
}
|
||||
else if (v1Val > v2Val) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
@@ -119,46 +149,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == getPlatform()) {
|
||||
|
||||
// Check the kernel version minimum if one exists
|
||||
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||
QVector<int> requiredVersionQuad;
|
||||
QVector<int> actualVersionQuad;
|
||||
|
||||
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||
QString actualVersion = QSysInfo::kernelVersion();
|
||||
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||
|
||||
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Found update manifest match for current platform";
|
||||
|
||||
QString latestVersion = updateObj["version"].toString();
|
||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||
|
||||
QVector<int> latestVersionQuad;
|
||||
|
||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int latestVer = 0;
|
||||
int currentVer = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < latestVersionQuad.count()) {
|
||||
latestVer = latestVersionQuad[i];
|
||||
}
|
||||
if (i < m_CurrentVersionQuad.count()) {
|
||||
currentVer = m_CurrentVersionQuad[i];
|
||||
}
|
||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentVer < latestVer) {
|
||||
qDebug() << "Update available";
|
||||
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (currentVer > latestVer) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||
if (res < 0) {
|
||||
// m_CurrentVersionQuad < latestVersionQuad
|
||||
qDebug() << "Update available";
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (res > 0) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
else {
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -166,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform: "
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
||||
qWarning() << "No entry in update manifest found for current platform:"
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
|
||||
@@ -20,6 +20,8 @@ private slots:
|
||||
private:
|
||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||
|
||||
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
|
||||
@@ -46,8 +46,8 @@ public:
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||
}
|
||||
|
||||
signals:
|
||||
|
||||
@@ -146,7 +146,8 @@ private:
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr)
|
||||
m_MdnsBrowser(nullptr),
|
||||
m_CompatFetcher(nullptr)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
@@ -159,12 +160,15 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Fetch latest compatibility data asynchronously
|
||||
m_CompatFetcher.start();
|
||||
|
||||
// To quit in a timely manner, we must block additional requests
|
||||
// after we receive the aboutToQuit() signal. This is neccessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
@@ -302,8 +306,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
|
||||
if (!pollingEntry->isActive()) {
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||
this, &ComputerManager::handleComputerStateChanged);
|
||||
pollingEntry->setActiveThread(thread);
|
||||
thread->start();
|
||||
}
|
||||
@@ -337,7 +341,6 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +421,7 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
{
|
||||
{
|
||||
QWriteLocker(&computer->lock);
|
||||
QWriteLocker lock(&computer->lock);
|
||||
|
||||
computer->name = name;
|
||||
computer->hasCustomName = true;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "nvcomputer.h"
|
||||
#include "nvpairingmanager.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
#include <qmdnsengine/cache.h>
|
||||
@@ -70,7 +71,7 @@ private:
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
@@ -214,4 +215,5 @@ private:
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
};
|
||||
|
||||
@@ -143,7 +143,10 @@ QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||
// the Steam Link hardware.
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
@@ -30,6 +31,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
@@ -49,6 +51,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
this->gpuModel = nullptr;
|
||||
this->isSupportedServerVersion = true;
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings) const
|
||||
@@ -98,7 +101,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
for (const QString& macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
@@ -142,6 +145,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
this->activeAddress = address;
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
@@ -407,6 +411,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
bool isSupportedServerVersion;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
|
||||
+11
-5
@@ -67,7 +67,8 @@ NvHTTP::parseQuad(QString quad)
|
||||
}
|
||||
|
||||
QStringList parts = quad.split(".");
|
||||
for (int i = 0; i < 4; i++)
|
||||
ret.reserve(parts.length());
|
||||
for (int i = 0; i < parts.length(); i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
@@ -391,7 +392,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto error : errors) {
|
||||
for (const QSslError& error : errors) {
|
||||
if (m_ServerCert != error.certificate()) {
|
||||
ignoreErrors = false;
|
||||
break;
|
||||
@@ -450,6 +451,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
@@ -462,10 +468,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
|
||||
@@ -215,7 +215,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
SystemProperties::SystemProperties()
|
||||
{
|
||||
versionString = QString(VERSION_STR);
|
||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
@@ -55,12 +55,8 @@ SystemProperties::SystemProperties()
|
||||
friendlyNativeArchName = nativeArch.toUpper();
|
||||
}
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasWindowManager;
|
||||
#else
|
||||
hasBrowser = false;
|
||||
#endif
|
||||
hasBrowser = hasDesktopEnvironment;
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
hasDiscordIntegration = true;
|
||||
@@ -91,7 +87,36 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
{
|
||||
public:
|
||||
QuerySdlVideoThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->querySdlVideoInfoInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
QuerySdlVideoThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
querySdlVideoInfoInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::querySdlVideoInfoInternal()
|
||||
{
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
@@ -103,7 +128,8 @@ void SystemProperties::querySdlVideoInfo()
|
||||
}
|
||||
|
||||
// Update display related attributes (max FPS, native resolution, etc).
|
||||
refreshDisplays();
|
||||
// We call the internal variant because we're already in a safe thread context.
|
||||
refreshDisplaysInternal();
|
||||
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
@@ -129,7 +155,36 @@ void SystemProperties::querySdlVideoInfo()
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
class RefreshDisplaysThread : public QThread
|
||||
{
|
||||
public:
|
||||
RefreshDisplaysThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->refreshDisplaysInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::refreshDisplays()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
RefreshDisplaysThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
refreshDisplaysInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::refreshDisplaysInternal()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
||||
@@ -7,6 +7,9 @@ class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class QuerySdlVideoThread;
|
||||
friend class RefreshDisplaysThread;
|
||||
|
||||
public:
|
||||
SystemProperties();
|
||||
|
||||
@@ -16,7 +19,7 @@ public:
|
||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
|
||||
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
@@ -33,6 +36,8 @@ signals:
|
||||
|
||||
private:
|
||||
void querySdlVideoInfo();
|
||||
void querySdlVideoInfoInternal();
|
||||
void refreshDisplaysInternal();
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
@@ -40,7 +45,7 @@ private:
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
QString friendlyNativeArchName;
|
||||
bool hasWindowManager;
|
||||
bool hasDesktopEnvironment;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
if (!unknownOptionNames().isEmpty()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
};
|
||||
m_CaptureSysKeysModeMap = {
|
||||
{"never", StreamingPreferences::CSK_OFF},
|
||||
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
@@ -307,7 +312,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode");
|
||||
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
@@ -320,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
@@ -422,8 +423,10 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --capture-system-keys and --no-capture-system-keys options
|
||||
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys);
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
}
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
|
||||
@@ -53,4 +53,5 @@ private:
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>7.1 surround sound audio support</li>
|
||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode on X11</li>
|
||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||
<li>Direct touchscreen mouse control</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||
@@ -36,6 +36,51 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="3.1.2" date="2021-05-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||
<li>Fixed keyboard capture on X11</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.1" date="2021-04-09">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||
<li>Added new community-contributed translations from Weblate</li>
|
||||
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.0" date="2021-03-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||
<li>Added translations for Simplified Chinese and German</li>
|
||||
<li>Added a language selector on the settings page</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance of loading the app grid</li>
|
||||
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||
<li>Fixed window icons on Wayland</li>
|
||||
<li>Fixed number pad enter key functionality in the UI</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.0" date="2021-01-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
|
||||
@@ -5,19 +5,9 @@
|
||||
# pairing data will be lost each reboot.
|
||||
HOME=/usr/local/moonlight
|
||||
|
||||
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
|
||||
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
|
||||
# the correct one at runtime.
|
||||
if [ -d "/usr/local/Qt-5.14.1" ]; then
|
||||
BIN="moonlight"
|
||||
else
|
||||
BIN="moonlight59"
|
||||
fi
|
||||
|
||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||
renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
echo Launching $BIN
|
||||
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
|
||||
+136
-100
@@ -73,6 +73,9 @@ CenteredGridView {
|
||||
width: 220; height: 287;
|
||||
grid: appGrid
|
||||
|
||||
property alias appContextMenu: appContextMenuLoader.item
|
||||
property alias appNameText: appNameTextLoader.item
|
||||
|
||||
// Dim the app if it's hidden
|
||||
opacity: model.hidden ? 0.4 : 1.0
|
||||
|
||||
@@ -85,9 +88,14 @@ CenteredGridView {
|
||||
source: model.boxart
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
||||
// the image size checks if this is not an app collector game. We know the officially
|
||||
// supported games all have box art, so this check is not required.
|
||||
if (!model.isAppCollectorGame &&
|
||||
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
|
||||
{
|
||||
isPlaceholder = true
|
||||
}
|
||||
@@ -104,81 +112,94 @@ CenteredGridView {
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: resumeButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
active: model.running
|
||||
asynchronous: true
|
||||
anchors.fill: appIcon
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: quitButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
id: appNameTextLoader
|
||||
active: appIcon.isPlaceholder
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
// This loader is not asynchronous to avoid noticeable differences
|
||||
// in the time in which the text loads for each game.
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Label {
|
||||
id: appNameText
|
||||
visible: appIcon.isPlaceholder
|
||||
text: model.name
|
||||
width: appIcon.width
|
||||
height: model.running ? 175 : appIcon.height
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
|
||||
anchors.left: appIcon.left
|
||||
anchors.right: appIcon.right
|
||||
anchors.bottom: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
sourceComponent: Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
font.pointSize: 22
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
@@ -245,6 +266,17 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
// Open the app context menu if activated via the gamepad or keyboard
|
||||
// for running games. If the game isn't running, the above onClicked
|
||||
// method will handle the launch.
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
@@ -256,44 +288,48 @@ CenteredGridView {
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
Loader {
|
||||
id: appContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,7 @@ ItemDelegate {
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ MenuItem {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ NavigableDialog {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ ToolButton {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
+60
-45
@@ -113,6 +113,8 @@ CenteredGridView {
|
||||
width: 300; height: 320;
|
||||
grid: pcGrid
|
||||
|
||||
property alias pcContextMenu : pcContextMenuLoader.item
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -160,61 +162,70 @@ CenteredGridView {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
Loader {
|
||||
id: pcContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.online) {
|
||||
if (model.paired) {
|
||||
if (!model.serverSupported) {
|
||||
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
else if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
@@ -379,6 +390,10 @@ CenteredGridView {
|
||||
Keys.onReturnPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+189
-46
@@ -358,6 +358,10 @@ Flickable {
|
||||
Keys.onReturnPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -382,6 +386,10 @@ Flickable {
|
||||
Keys.onReturnPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,12 +527,17 @@ Flickable {
|
||||
text: qsTr("Display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
// Set the recommended option based on the OS
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
@@ -537,22 +550,20 @@ Flickable {
|
||||
|
||||
currentIndex = 0
|
||||
|
||||
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) {
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: windowModeComboBox
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
enabled: !SystemProperties.rendererAlwaysFullScreen
|
||||
hoverEnabled: true
|
||||
textRole: "text"
|
||||
@@ -697,7 +708,7 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Mute audio stream when Moonlight is not the active window")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.muteOnFocusLoss
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.muteOnFocusLoss = checked
|
||||
@@ -724,37 +735,116 @@ Flickable {
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: uiDisplayModeTitle
|
||||
text: qsTr("GUI display mode")
|
||||
id: languageTitle
|
||||
text: qsTr("Language")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasWindowManager
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.hasWindowManager) {
|
||||
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
||||
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
||||
if (saved_uidisplaymode === el_uidisplaymode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
var saved_language = StreamingPreferences.language
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < languageListModel.count; i++) {
|
||||
var el_language = languageListModel.get(i).val;
|
||||
if (saved_language === el_language) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Full-screen is always selected when there is no window manager
|
||||
currentIndex = 2
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: languageComboBox
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: languageListModel
|
||||
ListElement {
|
||||
text: qsTr("Automatic")
|
||||
val: StreamingPreferences.LANG_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Deutsch" //German
|
||||
val: StreamingPreferences.LANG_DE
|
||||
}
|
||||
ListElement {
|
||||
text: "English"
|
||||
val: StreamingPreferences.LANG_EN
|
||||
}
|
||||
ListElement {
|
||||
text: "Français" //French
|
||||
val: StreamingPreferences.LANG_FR
|
||||
}
|
||||
ListElement {
|
||||
text: "简体中文" //Simplified Chinese
|
||||
val: StreamingPreferences.LANG_ZH_CN
|
||||
}
|
||||
ListElement {
|
||||
text: "Norwegian Bokmål"
|
||||
val: StreamingPreferences.LANG_NB_NO
|
||||
}
|
||||
ListElement {
|
||||
text: "русский" // Russian
|
||||
val: StreamingPreferences.LANG_RU
|
||||
}
|
||||
// Don't list Spanish until it is more complete
|
||||
/*ListElement {
|
||||
text: "Español" // Spanish
|
||||
val: StreamingPreferences.LANG_ES
|
||||
}*/
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
// Retranslating is expensive, so only do it if the language actually changed
|
||||
var new_language = languageListModel.get(currentIndex).val
|
||||
if (StreamingPreferences.language !== new_language) {
|
||||
StreamingPreferences.language = languageListModel.get(currentIndex).val
|
||||
if (!StreamingPreferences.retranslate()) {
|
||||
ToolTip.show(qsTr("You must restart Moonlight for this change to take effect"), 5000)
|
||||
}
|
||||
else {
|
||||
// Force the back operation to pop any AppView pages that exist.
|
||||
// The AppView stops working after retranslate() for some reason.
|
||||
window.clearOnBack = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: uiDisplayModeTitle
|
||||
text: qsTr("GUI display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
||||
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
||||
if (saved_uidisplaymode === el_uidisplaymode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: uiDisplayModeComboBox
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: uiDisplayModeListModel
|
||||
@@ -833,7 +923,6 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Optimize mouse for remote desktop instead of games")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
checked: StreamingPreferences.absoluteMouseMode
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.absoluteMouseMode = checked
|
||||
@@ -847,25 +936,79 @@ Flickable {
|
||||
qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: captureSysKeysCheck
|
||||
hoverEnabled: true
|
||||
Row {
|
||||
spacing: 5
|
||||
width: parent.width
|
||||
text: qsTr("Capture system keyboard shortcuts ") +
|
||||
(!SystemProperties.isRunningWayland ? qsTr("while streaming in fullscreen") :
|
||||
qsTr("(unsupported on Wayland)"))
|
||||
font.pointSize: 12
|
||||
enabled: SystemProperties.hasWindowManager && !SystemProperties.isRunningWayland
|
||||
checked: (StreamingPreferences.captureSysKeys || !SystemProperties.hasWindowManager) && !SystemProperties.isRunningWayland
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.captureSysKeys = checked
|
||||
|
||||
CheckBox {
|
||||
id: captureSysKeysCheck
|
||||
hoverEnabled: true
|
||||
text: qsTr("Capture system keyboard shortcuts")
|
||||
font.pointSize: 12
|
||||
enabled: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasDesktopEnvironment
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 10000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" +
|
||||
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 10000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.") + "\n\n" +
|
||||
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
var saved_syskeysmode = StreamingPreferences.captureSysKeysMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < captureSysKeysModeListModel.count; i++) {
|
||||
var el_syskeysmode = captureSysKeysModeListModel.get(i).val;
|
||||
if (saved_syskeysmode === el_syskeysmode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
enabled: captureSysKeysCheck.checked && captureSysKeysCheck.enabled
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: captureSysKeysModeListModel
|
||||
ListElement {
|
||||
text: qsTr("in fullscreen")
|
||||
val: StreamingPreferences.CSK_FULLSCREEN
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("always")
|
||||
val: StreamingPreferences.CSK_ALWAYS
|
||||
}
|
||||
}
|
||||
|
||||
function updatePref() {
|
||||
if (!enabled) {
|
||||
StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF
|
||||
}
|
||||
else {
|
||||
StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated: {
|
||||
updatePref()
|
||||
}
|
||||
|
||||
// This handles transition of the checkbox state
|
||||
onEnabledChanged: {
|
||||
updatePref()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
@@ -980,7 +1123,7 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Process gamepad input when Moonlight is in the background")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.backgroundGamepad
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.backgroundGamepad = checked
|
||||
|
||||
@@ -91,6 +91,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
return app.id;
|
||||
case DirectLaunchRole:
|
||||
return app.directLaunch;
|
||||
case AppCollectorGameRole:
|
||||
return app.isAppCollectorGame;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -106,6 +108,7 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
||||
names[HiddenRole] = "hidden";
|
||||
names[AppIdRole] = "appid";
|
||||
names[DirectLaunchRole] = "directLaunch";
|
||||
names[AppCollectorGameRole] = "appCollectorGame";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ class AppModel : public QAbstractListModel
|
||||
BoxArtRole,
|
||||
HiddenRole,
|
||||
AppIdRole,
|
||||
DirectLaunchRole
|
||||
DirectLaunchRole,
|
||||
AppCollectorGameRole,
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
@@ -42,6 +42,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
return !computer->macAddress.isEmpty();
|
||||
case StatusUnknownRole:
|
||||
return computer->state == NvComputer::CS_UNKNOWN;
|
||||
case ServerSupportedRole:
|
||||
return computer->isSupportedServerVersion;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -68,6 +70,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
names[BusyRole] = "busy";
|
||||
names[WakeableRole] = "wakeable";
|
||||
names[StatusUnknownRole] = "statusUnknown";
|
||||
names[ServerSupportedRole] = "serverSupported";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
|
||||
PairedRole,
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
StatusUnknownRole
|
||||
StatusUnknownRole,
|
||||
ServerSupportedRole
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
+24
-4
@@ -12,13 +12,18 @@ import SdlGamepadKeyNavigation 1.0
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
|
||||
// Set by SettingsView to force the back operation to pop all
|
||||
// pages except the initial view. This is required when doing
|
||||
// a retranslate() because AppView breaks for some reason.
|
||||
property bool clearOnBack: false
|
||||
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
visibility: {
|
||||
if (SystemProperties.hasWindowManager) {
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
||||
@@ -31,6 +36,17 @@ ApplicationWindow {
|
||||
// it will never insert a line break and just extend on forever.
|
||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
// Pop all items except the first one
|
||||
stackView.pop(null)
|
||||
clearOnBack = false
|
||||
}
|
||||
else {
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: initialView
|
||||
@@ -46,7 +62,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -55,7 +71,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onBackPressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -210,7 +226,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/arrow_left.svg"
|
||||
|
||||
onClicked: stackView.pop()
|
||||
onClicked: goBack()
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -495,6 +511,10 @@ ApplicationWindow {
|
||||
Keys.onReturnPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+403
-171
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+23
-14
@@ -11,7 +11,6 @@
|
||||
#include <QCursor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include<QTranslator>
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
@@ -351,14 +350,16 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) {
|
||||
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to
|
||||
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace())
|
||||
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like
|
||||
// Qt 5 does until we figure out the cause of these issues.
|
||||
qputenv("QSG_RHI_BACKEND", "opengl");
|
||||
}
|
||||
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Moonlight requires the non-threaded renderer because we depend
|
||||
// on being able to control the render thread by blocking in the
|
||||
// main thread (and pumping events from the main thread when needed).
|
||||
// That doesn't work with the threaded renderer which causes all
|
||||
// sorts of odd behavior depending on the platform.
|
||||
//
|
||||
// NB: Windows defaults to the "windows" non-threaded render loop on
|
||||
// Qt 5 and the threaded render loop on Qt 6.
|
||||
qputenv("QSG_RENDER_LOOP", "basic");
|
||||
#endif
|
||||
|
||||
// We don't want system proxies to apply to us
|
||||
@@ -394,6 +395,9 @@ int main(int argc, char *argv[])
|
||||
// future KMSDRM platforms that use SDL for rendering.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
||||
|
||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||
|
||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
||||
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||
@@ -451,9 +455,9 @@ int main(int argc, char *argv[])
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
QTranslator translator;
|
||||
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name());
|
||||
app.installTranslator(&translator);
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
@@ -499,6 +503,11 @@ int main(int argc, char *argv[])
|
||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
// Register our C++ types for QML
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
@@ -525,8 +534,8 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||
"StreamingPreferences",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences();
|
||||
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences(qmlEngine);
|
||||
});
|
||||
|
||||
// Create the identity manager on the main thread
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file contains hooks for several functions that allow
|
||||
// Qt and SDL to more or less share DRM master ownership.
|
||||
//
|
||||
// This technique requires Linux v5.8 or later, or for Moonlight
|
||||
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
|
||||
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
|
||||
// our trick from working (without root, that is).
|
||||
//
|
||||
// The specific kernel change required to run without root is:
|
||||
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// We require SDL 2.0.15+ to hook because it supports sharing
|
||||
// the DRM FD with our code. This avoids having multiple DRM FDs
|
||||
// in flight at the same time which would significantly complicate
|
||||
// the logic here because we'd need to figure out exactly which FD
|
||||
// should be the master at any given time. With the position of our
|
||||
// hooks, that is definitely not trivial.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
// This hook will handle legacy DRM rendering
|
||||
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
}
|
||||
|
||||
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||
// hook this variant of open(), since that's what SDL uses. When we see
|
||||
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||
// master on the Qt FD to allow the new FD to have master.
|
||||
int open(const char *pathname, int flags)
|
||||
{
|
||||
// Call the real thing to do the open operation
|
||||
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// It's a DRM device, but is it _our_ DRM device?
|
||||
struct stat64 fdstat;
|
||||
|
||||
fstat64(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Our close() hook handles restoring DRM master to the Qt FD
|
||||
// after SDL closes its DRM FD.
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
if (ret == 0) {
|
||||
// If we just closed the SDL DRM master FD, restore master
|
||||
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||
// was master once before, so we can set it as master again
|
||||
// using drmSetMaster() without CAP_SYS_ADMIN.
|
||||
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
|
||||
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
g_SdlDrmMasterFd = -1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
+14
-2
@@ -17,14 +17,26 @@
|
||||
<file>res/baseline-error_outline-24px.svg</file>
|
||||
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||
<file>res/Discord-Logo-White.svg</file>
|
||||
<file>languages/qml_de.ts</file>
|
||||
<file>languages/qml_de.qm</file>
|
||||
<file>languages/qml_fr.ts</file>
|
||||
<file>languages/qml_fr.qm</file>
|
||||
<file>languages/qml_zh_cn.ts</file>
|
||||
<file>languages/qml_zh_cn.qm</file>
|
||||
<file>languages/qml_nb_NO.ts</file>
|
||||
<file>languages/qml_nb_NO.qm</file>
|
||||
<file>languages/qml_ru.ts</file>
|
||||
<file>languages/qml_ru.qm</file>
|
||||
<file>languages/qml_es.ts</file>
|
||||
<file>languages/qml_es.qm</file>
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||
<file alias="egl.frag">shaders/egl.frag</file>
|
||||
<file alias="egl.vert">shaders/egl.vert</file>
|
||||
<file alias="egl_nv12.frag">shaders/egl_nv12.frag</file>
|
||||
<file alias="egl_nv12.vert">shaders/egl_nv12.vert</file>
|
||||
<file alias="egl_opaque.frag">shaders/egl_opaque.frag</file>
|
||||
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
|
||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||
</qresource>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "compatfetcher.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
|
||||
#define COMPAT_VERSION "v1"
|
||||
#define COMPAT_KEY "latestsupportedversion-"
|
||||
|
||||
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
{
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &CompatFetcher::handleCompatInfoFetched);
|
||||
}
|
||||
|
||||
void CompatFetcher::start()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QUrl url("https://moonlight-stream.org/compatibility/" COMPAT_VERSION);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
}
|
||||
|
||||
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
if (gfeVersion.isEmpty()) {
|
||||
// If we don't have a GFE version, just allow it
|
||||
return true;
|
||||
}
|
||||
|
||||
QString latestSupportedVersion = settings.value(COMPAT_KEY COMPAT_VERSION).toString();
|
||||
if (latestSupportedVersion.isEmpty()) {
|
||||
// We don't have compat data yet, so just assume it's supported
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList latestSupportedVersionQuad = latestSupportedVersion.split('.');
|
||||
QStringList gfeVersionQuad = gfeVersion.split('.');
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int actualVerVal = 0;
|
||||
int latestSupportedVal = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < gfeVersionQuad.count()) {
|
||||
actualVerVal = gfeVersionQuad[i].toInt();
|
||||
}
|
||||
if (i < latestSupportedVersionQuad.count()) {
|
||||
latestSupportedVal = latestSupportedVersionQuad[i].toInt();
|
||||
}
|
||||
|
||||
if (i >= gfeVersionQuad.count() && i >= latestSupportedVersionQuad.count()) {
|
||||
// Equal versions - this is fine
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actualVerVal < latestSupportedVal) {
|
||||
// Actual version is lower than latest supported - this is fine
|
||||
return true;
|
||||
}
|
||||
else if (actualVerVal > latestSupportedVal) {
|
||||
// Actual version is greater than latest supported - this is bad
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
QString version = QString(reply->readAll()).trimmed();
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue(COMPAT_KEY COMPAT_VERSION, version);
|
||||
|
||||
qInfo() << "Latest supported GFE server:" << version;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Failed to download latest compatibility data:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class CompatFetcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CompatFetcher(QObject *parent = nullptr);
|
||||
|
||||
void start();
|
||||
|
||||
static bool isGfeVersionSupported(QString gfeVersion);
|
||||
|
||||
private slots:
|
||||
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -13,8 +13,8 @@ MappingFetcher::MappingFetcher(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleMappingListFetched(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &MappingFetcher::handleMappingListFetched);
|
||||
}
|
||||
|
||||
void MappingFetcher::start()
|
||||
|
||||
@@ -41,7 +41,7 @@ MappingManager::MappingManager()
|
||||
#else
|
||||
.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
for (QString sdlMapping : sdlMappings) {
|
||||
for (const QString& sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "streamingpreferences.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
#include <QLocale>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#define SER_STREAMSETTINGS "streamsettings"
|
||||
#define SER_WIDTH "width"
|
||||
@@ -36,11 +41,20 @@
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
#define SER_LANGUAGE "language"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(nullptr)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(qmlEngine)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
@@ -81,7 +95,8 @@ void StreamingPreferences::reload()
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool();
|
||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -95,6 +110,8 @@ void StreamingPreferences::reload()
|
||||
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
||||
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
||||
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
||||
language = static_cast<Language>(settings.value(SER_LANGUAGE,
|
||||
static_cast<int>(Language::LANG_AUTO)).toInt());
|
||||
|
||||
|
||||
// Perform default settings updates as required based on last default version
|
||||
@@ -108,6 +125,82 @@ void StreamingPreferences::reload()
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamingPreferences::retranslate()
|
||||
{
|
||||
static QTranslator* translator = nullptr;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
if (m_QmlEngine != nullptr) {
|
||||
// Dynamic retranslation is not supported until Qt 5.10
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
QTranslator* newTranslator = new QTranslator();
|
||||
QString languageSuffix = getSuffixFromLanguage(language);
|
||||
|
||||
// Remove the old translator, even if we can't load a new one.
|
||||
// Otherwise we'll be stuck with the old translated values instead
|
||||
// of defaulting to English.
|
||||
if (translator != nullptr) {
|
||||
QCoreApplication::removeTranslator(translator);
|
||||
delete translator;
|
||||
translator = nullptr;
|
||||
}
|
||||
|
||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||
|
||||
translator = newTranslator;
|
||||
QCoreApplication::installTranslator(translator);
|
||||
}
|
||||
else {
|
||||
qInfo() << "No translation available for " << languageSuffix;
|
||||
delete newTranslator;
|
||||
}
|
||||
|
||||
if (m_QmlEngine != nullptr) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
// This is a dynamic retranslation from the settings page.
|
||||
// We have to kick the QML engine into reloading our text.
|
||||
m_QmlEngine->retranslate();
|
||||
#else
|
||||
// Unreachable below Qt 5.10 due to the check above
|
||||
Q_ASSERT(false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// This is a translation from a non-QML context, which means
|
||||
// it is probably app startup. There's nothing to refresh.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
|
||||
{
|
||||
switch (lang)
|
||||
{
|
||||
case LANG_DE:
|
||||
return "de";
|
||||
case LANG_EN:
|
||||
return "en";
|
||||
case LANG_FR:
|
||||
return "fr";
|
||||
case LANG_ZH_CN:
|
||||
return "zh_cn";
|
||||
case LANG_NB_NO:
|
||||
return "nb_NO";
|
||||
case LANG_RU:
|
||||
return "ru";
|
||||
case LANG_ES:
|
||||
return "es";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
{
|
||||
QSettings settings;
|
||||
@@ -136,13 +229,14 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
||||
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
|
||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
||||
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class StreamingPreferences : public QObject
|
||||
{
|
||||
@@ -9,6 +10,7 @@ class StreamingPreferences : public QObject
|
||||
|
||||
public:
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
@@ -58,6 +60,30 @@ public:
|
||||
};
|
||||
Q_ENUM(UIDisplayMode)
|
||||
|
||||
// New entries must go at the end of the enum
|
||||
// to avoid renumbering existing entries (which
|
||||
// would affect existing user preferences).
|
||||
enum Language
|
||||
{
|
||||
LANG_AUTO,
|
||||
LANG_EN,
|
||||
LANG_FR,
|
||||
LANG_ZH_CN,
|
||||
LANG_DE,
|
||||
LANG_NB_NO,
|
||||
LANG_RU,
|
||||
LANG_ES
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
enum CaptureSysKeysMode
|
||||
{
|
||||
CSK_OFF,
|
||||
CSK_FULLSCREEN,
|
||||
CSK_ALWAYS,
|
||||
};
|
||||
Q_ENUM(CaptureSysKeysMode);
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
@@ -87,7 +113,10 @@ public:
|
||||
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
||||
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
||||
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
||||
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
|
||||
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||
|
||||
Q_INVOKABLE bool retranslate();
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -113,7 +142,6 @@ public:
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
bool captureSysKeys;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
@@ -121,6 +149,8 @@ public:
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
Language language;
|
||||
CaptureSysKeysMode captureSysKeysMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -149,6 +179,12 @@ signals:
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysChanged();
|
||||
void captureSysKeysModeChanged();
|
||||
void languageChanged();
|
||||
|
||||
private:
|
||||
QString getSuffixFromLanguage(Language lang);
|
||||
|
||||
QQmlEngine* m_QmlEngine;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 vTextCoord;
|
||||
|
||||
uniform samplerExternalOES uTexture;
|
||||
|
||||
void main() {
|
||||
FragColor = texture2D(uTexture, vTextCoord);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 300 es
|
||||
|
||||
layout (location = 0) in vec2 aPosition; // 2D: X,Y
|
||||
layout (location = 1) in vec2 aTexCoord;
|
||||
out vec2 vTextCoord;
|
||||
|
||||
void main() {
|
||||
vTextCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
#include "qtaud.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
QtAudioRenderer::QtAudioRenderer()
|
||||
: m_AudioOutput(nullptr),
|
||||
m_OutputDevice(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QtAudioRenderer::~QtAudioRenderer()
|
||||
{
|
||||
delete m_AudioOutput;
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
// testAudio() assumes this sample rate
|
||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||
|
||||
format.setSampleRate(opusConfig->sampleRate);
|
||||
format.setChannelCount(opusConfig->channelCount);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
m_AudioOutput = new QAudioOutput(format);
|
||||
|
||||
// Buffer 40 ms of audio data
|
||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||
|
||||
m_OutputDevice = m_AudioOutput->start();
|
||||
if (m_OutputDevice == nullptr) {
|
||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what buffer size was really used after starting
|
||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
switch (audioConfiguration) {
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
format.setChannelCount(2);
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
format.setChannelCount(6);
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QtAudioRenderer::detectAudioConfiguration()
|
||||
{
|
||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||
|
||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||
|
||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||
if (preferredChannelCount > 2) {
|
||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <QAudioOutput>
|
||||
|
||||
class QtAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
QtAudioRenderer();
|
||||
|
||||
virtual ~QtAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
virtual bool testAudio(int audioConfiguration);
|
||||
|
||||
virtual int detectAudioConfiguration();
|
||||
|
||||
private:
|
||||
QAudioOutput* m_AudioOutput;
|
||||
QIODevice* m_OutputDevice;
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
int SdlInputHandler::clipboardThreadProc(void *ptr)
|
||||
{
|
||||
auto me = (SdlInputHandler*)ptr;
|
||||
|
||||
while (!SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||
QString clipboardData;
|
||||
|
||||
SDL_LockMutex(me->m_ClipboardLock);
|
||||
for (;;) {
|
||||
clipboardData = me->m_ClipboardData;
|
||||
me->m_ClipboardData.clear();
|
||||
if (!clipboardData.isEmpty() || SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||
break;
|
||||
}
|
||||
SDL_CondWait(me->m_ClipboardHasData, me->m_ClipboardLock);
|
||||
}
|
||||
SDL_UnlockMutex(me->m_ClipboardLock);
|
||||
|
||||
// We might get here on shutdown, so don't send text if there's
|
||||
// nothing to send.
|
||||
if (!clipboardData.isEmpty()) {
|
||||
me->sendText(clipboardData);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define MAP_KEY(c, sc) \
|
||||
case c: \
|
||||
event.key.keysym.scancode = sc; \
|
||||
break
|
||||
|
||||
#define MAP_KEY_SHIFT(c, sc) \
|
||||
case c: \
|
||||
event.key.keysym.scancode = sc; \
|
||||
event.key.keysym.mod = KMOD_SHIFT; \
|
||||
break
|
||||
|
||||
void SdlInputHandler::sendText(QString& string)
|
||||
{
|
||||
for (int i = 0; i < string.size(); i++) {
|
||||
char16_t c = string[i].unicode();
|
||||
SDL_Event event = {};
|
||||
|
||||
// If we're sending a very long string, we might get a termination request
|
||||
// while we're in the middle of sending a string. In that case, just bail.
|
||||
if (SDL_AtomicGet(&m_ShutdownClipboardThread)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - 'A') + SDL_SCANCODE_A);
|
||||
event.key.keysym.mod = KMOD_SHIFT;
|
||||
}
|
||||
else if (c >= 'a' && c <= 'z') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - 'a') + SDL_SCANCODE_A);
|
||||
}
|
||||
else if (c >= '1' && c <= '9') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - '1') + SDL_SCANCODE_1);
|
||||
}
|
||||
else {
|
||||
// Fixup some Unicode characters to be expressible in ASCII
|
||||
switch (c) {
|
||||
case u'\U0000201C': // Left "
|
||||
case u'\U0000201D': // Right "
|
||||
c = '"';
|
||||
break;
|
||||
|
||||
case u'\U00002018': // Left '
|
||||
case u'\U00002019': // Right '
|
||||
c = '\'';
|
||||
break;
|
||||
|
||||
case u'\U00002013': // Smart -
|
||||
c = '-';
|
||||
break;
|
||||
|
||||
case u'\U00002026': // Ellipsis (...)
|
||||
// Convert this character into 3 periods
|
||||
string.insert(i+1, '.');
|
||||
string.insert(i+1, '.');
|
||||
c = '.';
|
||||
break;
|
||||
|
||||
case u'\U000000A0': // Non-breaking space
|
||||
c = ' ';
|
||||
break;
|
||||
|
||||
default:
|
||||
// Nothing
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
switch (c) {
|
||||
|
||||
// Handle CRLF separately to avoid duplicate newlines
|
||||
case '\r':
|
||||
if (string[i + 1] == '\n') {
|
||||
i++;
|
||||
}
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
|
||||
break;
|
||||
|
||||
MAP_KEY('\b', SDL_SCANCODE_BACKSPACE);
|
||||
MAP_KEY('\n', SDL_SCANCODE_RETURN);
|
||||
MAP_KEY('\t', SDL_SCANCODE_TAB);
|
||||
|
||||
MAP_KEY(' ', SDL_SCANCODE_SPACE);
|
||||
MAP_KEY_SHIFT('!', SDL_SCANCODE_1);
|
||||
MAP_KEY_SHIFT('"', SDL_SCANCODE_APOSTROPHE);
|
||||
MAP_KEY_SHIFT('#', SDL_SCANCODE_3);
|
||||
MAP_KEY_SHIFT('$', SDL_SCANCODE_4);
|
||||
MAP_KEY_SHIFT('%', SDL_SCANCODE_5);
|
||||
MAP_KEY_SHIFT('&', SDL_SCANCODE_7);
|
||||
MAP_KEY('\'', SDL_SCANCODE_APOSTROPHE);
|
||||
MAP_KEY_SHIFT('(', SDL_SCANCODE_9);
|
||||
MAP_KEY_SHIFT(')', SDL_SCANCODE_0);
|
||||
MAP_KEY_SHIFT('*', SDL_SCANCODE_8);
|
||||
MAP_KEY_SHIFT('+', SDL_SCANCODE_EQUALS);
|
||||
MAP_KEY(',', SDL_SCANCODE_COMMA);
|
||||
MAP_KEY('-', SDL_SCANCODE_MINUS);
|
||||
MAP_KEY('.', SDL_SCANCODE_PERIOD);
|
||||
MAP_KEY('/', SDL_SCANCODE_SLASH);
|
||||
MAP_KEY('0', SDL_SCANCODE_0);
|
||||
|
||||
MAP_KEY_SHIFT(':', SDL_SCANCODE_SEMICOLON);
|
||||
MAP_KEY(';', SDL_SCANCODE_SEMICOLON);
|
||||
MAP_KEY_SHIFT('<', SDL_SCANCODE_COMMA);
|
||||
MAP_KEY('=', SDL_SCANCODE_EQUALS);
|
||||
MAP_KEY_SHIFT('>', SDL_SCANCODE_PERIOD);
|
||||
MAP_KEY_SHIFT('?', SDL_SCANCODE_SLASH);
|
||||
MAP_KEY_SHIFT('@', SDL_SCANCODE_2);
|
||||
|
||||
MAP_KEY('[', SDL_SCANCODE_LEFTBRACKET);
|
||||
MAP_KEY('\\', SDL_SCANCODE_BACKSLASH);
|
||||
MAP_KEY(']', SDL_SCANCODE_RIGHTBRACKET);
|
||||
MAP_KEY_SHIFT('^', SDL_SCANCODE_6);
|
||||
MAP_KEY_SHIFT('_', SDL_SCANCODE_MINUS);
|
||||
MAP_KEY('`', SDL_SCANCODE_GRAVE);
|
||||
|
||||
MAP_KEY_SHIFT('{', SDL_SCANCODE_LEFTBRACKET);
|
||||
MAP_KEY_SHIFT('|', SDL_SCANCODE_BACKSLASH);
|
||||
MAP_KEY_SHIFT('}', SDL_SCANCODE_RIGHTBRACKET);
|
||||
MAP_KEY_SHIFT('~', SDL_SCANCODE_GRAVE);
|
||||
|
||||
default:
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Pasting text - non-ASCII character value 'U+%x' ignored",
|
||||
c);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||
SDL_Event modifierEvent = {};
|
||||
modifierEvent.type = SDL_KEYDOWN;
|
||||
modifierEvent.key.state = SDL_PRESSED;
|
||||
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||
handleKeyEvent(&modifierEvent.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
}
|
||||
|
||||
event.type = SDL_KEYDOWN;
|
||||
event.key.state = SDL_PRESSED;
|
||||
handleKeyEvent(&event.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
|
||||
event.type = SDL_KEYUP;
|
||||
event.key.state = SDL_RELEASED;
|
||||
handleKeyEvent(&event.key);
|
||||
|
||||
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||
SDL_Event modifierEvent = {};
|
||||
modifierEvent.type = SDL_KEYUP;
|
||||
modifierEvent.key.state = SDL_RELEASED;
|
||||
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||
handleKeyEvent(&modifierEvent.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Clear buttons down on this gameapd
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Select+L1+R1+X as a gamepad overlay combo
|
||||
if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | X_FLAG)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle gamepad combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
|
||||
+197
-199
@@ -7,13 +7,10 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define MOUSE_POLLING_INTERVAL 5
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
HHOOK g_KeyboardHook;
|
||||
#endif
|
||||
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||
: m_MultiController(prefs.multiController),
|
||||
m_GamepadMouse(prefs.gamepadMouse),
|
||||
@@ -25,19 +22,25 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_FakeCaptureActive(false),
|
||||
m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()),
|
||||
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
|
||||
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
|
||||
m_LongPressTimer(0),
|
||||
m_StreamWidth(streamWidth),
|
||||
m_StreamHeight(streamHeight),
|
||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||
m_PendingMouseLeaveButtonUp(0),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
m_DragButton(0),
|
||||
m_NumFingersDown(0)
|
||||
m_NumFingersDown(0),
|
||||
m_ClipboardData()
|
||||
{
|
||||
// System keys are always captured when running without a DE
|
||||
if (!WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS;
|
||||
}
|
||||
|
||||
// Allow gamepad input when the app doesn't have focus if requested
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
||||
|
||||
@@ -49,10 +52,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
prefs.absoluteMouseMode ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
|
||||
// If we're grabbing system keys, enable the X11 keyboard grab in SDL
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD
|
||||
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
||||
m_CaptureSystemKeysEnabled ? "1" : "0",
|
||||
m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
#endif
|
||||
|
||||
// Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled
|
||||
SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0");
|
||||
|
||||
// Allow clicks to pass through to us when focusing the window. If we're in
|
||||
// absolute mouse mode, this will avoid the user having to click twice to
|
||||
@@ -67,6 +75,47 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||
|
||||
// Populate special key combo configuration
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
|
||||
@@ -123,6 +172,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSGetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), &m_OldHotKeyMode);
|
||||
#endif
|
||||
|
||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||
// during stream startup as we detect currently attached gamepads one at a time.
|
||||
@@ -150,37 +203,16 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
|
||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
if (m_CaptureSystemKeysEnabled) {
|
||||
// If system key capture is enabled, install the window hook required to intercept
|
||||
// these key presses and block them from the OS itself.
|
||||
g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0);
|
||||
}
|
||||
#endif
|
||||
// Initialize state used by the clipboard thread before we start it
|
||||
SDL_AtomicSet(&m_ShutdownClipboardThread, 0);
|
||||
m_ClipboardHasData = SDL_CreateCond();
|
||||
m_ClipboardLock = SDL_CreateMutex();
|
||||
|
||||
m_ClipboardThread = SDL_CreateThread(SdlInputHandler::clipboardThreadProc, "Clipboard Sender", this);
|
||||
}
|
||||
|
||||
SdlInputHandler::~SdlInputHandler()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
if (g_KeyboardHook != nullptr) {
|
||||
UnhookWindowsHookEx(g_KeyboardHook);
|
||||
g_KeyboardHook = nullptr;
|
||||
|
||||
// If we're terminating because of the user pressing the quit combo,
|
||||
// we won't have a chance to inform SDL as the modifier keys raise.
|
||||
// This will leave stale modifier flags in the SDL_Keyboard global
|
||||
// inside SDL which will show up on our next key event if the user
|
||||
// streams again. Avoid this by explicitly zeroing mod state when
|
||||
// ending the stream.
|
||||
//
|
||||
// This is only needed for the case where we're hooking the keyboard
|
||||
// because we're generating synthetic SDL_KEYDOWN/SDL_KEYUP events
|
||||
// which don't properly maintain SDL's internal keyboard state,
|
||||
// so we're forced to invoke SDL_SetModState() ourselves to set mods.
|
||||
SDL_SetModState(KMOD_NONE);
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||
Session::get()->notifyMouseEmulationMode(false);
|
||||
@@ -202,6 +234,21 @@ SdlInputHandler::~SdlInputHandler()
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
|
||||
// Wake up the clipboard thread to terminate it
|
||||
SDL_AtomicSet(&m_ShutdownClipboardThread, 1);
|
||||
SDL_CondBroadcast(m_ClipboardHasData);
|
||||
|
||||
// Wait for it to terminate
|
||||
SDL_WaitThread(m_ClipboardThread, nullptr);
|
||||
|
||||
// Now we can safely clean up its resources
|
||||
SDL_DestroyCond(m_ClipboardHasData);
|
||||
SDL_DestroyMutex(m_ClipboardLock);
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||
@@ -234,157 +281,6 @@ void SdlInputHandler::setWindow(SDL_Window *window)
|
||||
m_Window = window;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
LRESULT CALLBACK SdlInputHandler::keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode < 0 || nCode != HC_ACTION) {
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
bool keyCaptureActive;
|
||||
SDL_Event event = {};
|
||||
KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam;
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
event.key.keysym.sym = SDLK_LGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LGUI;
|
||||
break;
|
||||
case VK_RWIN:
|
||||
event.key.keysym.sym = SDLK_RGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RGUI;
|
||||
break;
|
||||
case VK_LMENU:
|
||||
event.key.keysym.sym = SDLK_LALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LALT;
|
||||
break;
|
||||
case VK_RMENU:
|
||||
event.key.keysym.sym = SDLK_RALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RALT;
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
event.key.keysym.sym = SDLK_LCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LCTRL;
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
event.key.keysym.sym = SDLK_RCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RCTRL;
|
||||
break;
|
||||
default:
|
||||
// Bail quickly if it's not a key we care about
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
// Make sure we're in a state where we actually want to steal this event (and it is safe to do so)
|
||||
Session* session = Session::get();
|
||||
if (session == nullptr || session->m_InputHandler == nullptr) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
keyCaptureActive = session->m_InputHandler->isSystemKeyCaptureActive();
|
||||
|
||||
// If this is a key we're going to intercept, create the synthetic SDL event.
|
||||
// This is necessary because we are also hiding this key event from ourselves too.
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
event.type = SDL_KEYDOWN;
|
||||
event.key.state = SDL_PRESSED;
|
||||
}
|
||||
else {
|
||||
event.type = SDL_KEYUP;
|
||||
event.key.state = SDL_RELEASED;
|
||||
}
|
||||
|
||||
// Drop the event if it's a key down and capture is not active.
|
||||
// For key up, we'll need to do a little more work to determine if we need to send this
|
||||
// event to SDL in order to keep its internal keyboard modifier state consistent.
|
||||
if (event.type == SDL_KEYDOWN && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.keysym.mod = SDL_GetModState();
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LGUI;
|
||||
}
|
||||
break;
|
||||
case VK_RWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RGUI;
|
||||
}
|
||||
break;
|
||||
case VK_LMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LALT;
|
||||
}
|
||||
break;
|
||||
case VK_RMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RALT;
|
||||
}
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LCTRL;
|
||||
}
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RCTRL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the modifier state is unchanged and we're not capturing system keys,
|
||||
// drop the event. If the modifier state is changed, we need to send the
|
||||
// event to update SDL's state even if system key capture is now inactive
|
||||
// (due to focus loss, mouse capture toggled off, etc.) otherwise SDL won't
|
||||
// know that the modifier key has been lifted.
|
||||
if (event.key.keysym.mod == SDL_GetModState() && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.timestamp = SDL_GetTicks();
|
||||
event.key.windowID = SDL_GetWindowID(session->m_Window);
|
||||
|
||||
// NOTE: event.key.repeat is not populated in this path!
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Synchronize SDL's modifier state with the current state.
|
||||
// SDL won't do this on its own because it will never see the
|
||||
// WM_KEYUP/WM_KEYDOWN events.
|
||||
SDL_SetModState((SDL_Keymod)event.key.keysym.mod);
|
||||
|
||||
// Eat the event only if key capture is active.
|
||||
// If capture is not active and we're just resyncing SDL's modifier state,
|
||||
// we need to ensure the key event is still delivered normally to the
|
||||
// window in focus.
|
||||
if (keyCaptureActive) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
NextHook:
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::raiseAllKeys()
|
||||
{
|
||||
if (m_KeysDown.isEmpty()) {
|
||||
@@ -404,20 +300,22 @@ void SdlInputHandler::raiseAllKeys()
|
||||
|
||||
void SdlInputHandler::notifyMouseLeave()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to
|
||||
// allow us to receive the mouse button up events later.
|
||||
//
|
||||
// On macOS and X11, capturing the mouse allows us to receive mouse motion outside the
|
||||
// window (button up already worked without capture).
|
||||
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||
if (mouseState & SDL_BUTTON(button)) {
|
||||
m_PendingMouseLeaveButtonUp = button;
|
||||
SDL_CaptureMouse(SDL_TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusLost()
|
||||
@@ -430,11 +328,27 @@ void SdlInputHandler::notifyFocusLost()
|
||||
setCaptureActive(false);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Ungrab the keyboard
|
||||
updateKeyboardGrabState();
|
||||
#endif
|
||||
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
raiseAllKeys();
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusGained()
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Re-grab the keyboard if it was grabbed before focus loss
|
||||
// FIXME: We only do this on macOS because we get a spurious
|
||||
// focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows
|
||||
// immediately after losing focus by clicking on another window.
|
||||
updateKeyboardGrabState();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isCaptureActive()
|
||||
{
|
||||
if (SDL_GetRelativeMouseMode()) {
|
||||
@@ -445,9 +359,69 @@ bool SdlInputHandler::isCaptureActive()
|
||||
return m_FakeCaptureActive;
|
||||
}
|
||||
|
||||
void SdlInputHandler::updateKeyboardGrabState()
|
||||
{
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldGrab = isCaptureActive();
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
// Ungrab if it's fullscreen only and we left fullscreen
|
||||
shouldGrab = false;
|
||||
}
|
||||
#ifdef Q_OS_DARWIN
|
||||
else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) {
|
||||
// Ungrab if we lose input focus on macOS. SDL will handle
|
||||
// this internally for platforms where we use the SDL
|
||||
// SDL_SetWindowKeyboardGrab() API exclusively.
|
||||
//
|
||||
// NB: On X11, we may not have input focus at the time of
|
||||
// the initial keyboard grab update, so we must not enable
|
||||
// this codepath on Linux.
|
||||
shouldGrab = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Don't close the window on Alt+F4 when keyboard grab is enabled
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0");
|
||||
|
||||
if (shouldGrab) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland.
|
||||
// This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag
|
||||
// that we look for to see if keyboard capture is enabled. We'll handle macOS
|
||||
// ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API.
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
// If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet,
|
||||
// grab the cursor (will grab the keyboard too on X11).
|
||||
if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) {
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
#endif
|
||||
#ifdef Q_OS_DARWIN
|
||||
// SDL doesn't support this private macOS API
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the keyboard to leave the window
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
#ifdef Q_OS_DARWIN
|
||||
// SDL doesn't support this private macOS API
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
{
|
||||
if (!m_CaptureSystemKeysEnabled) {
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -456,19 +430,35 @@ bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
}
|
||||
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
return (windowFlags & SDL_WINDOW_INPUT_FOCUS) &&
|
||||
(windowFlags & SDL_WINDOW_INPUT_GRABBED) &&
|
||||
(windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|| !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED)
|
||||
#else
|
||||
|| !(windowFlags & SDL_WINDOW_INPUT_GRABBED)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdlInputHandler::setCaptureActive(bool active)
|
||||
{
|
||||
if (active) {
|
||||
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||
// If we're in full-screen desktop mode but system key capture is enabled, also grab the cursor (will grab the keyboard too on X11).
|
||||
if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) ||
|
||||
(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!m_AbsoluteMouseMode) {
|
||||
@@ -482,7 +472,7 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
m_FakeCaptureActive = true;
|
||||
}
|
||||
|
||||
@@ -515,9 +505,17 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
|
||||
#else
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Now update the keyboard grab
|
||||
updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||
|
||||
+60
-10
@@ -5,13 +5,9 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#define SDL_CODE_HIDE_CURSOR 1
|
||||
#define SDL_CODE_SHOW_CURSOR 2
|
||||
#define SDL_CODE_UNCAPTURE_MOUSE 3
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
@@ -33,6 +29,25 @@ struct GamepadState {
|
||||
unsigned char lt, rt;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include <CoreGraphics/CGError.h>
|
||||
extern "C" {
|
||||
typedef int CGSConnection;
|
||||
typedef enum {
|
||||
CGSGlobalHotKeyEnable = 0,
|
||||
CGSGlobalHotKeyDisable = 1,
|
||||
} CGSGlobalHotKeyOperatingMode;
|
||||
|
||||
extern CGSConnection _CGSDefaultConnection(void);
|
||||
|
||||
extern CGError CGSGetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||
CGSGlobalHotKeyOperatingMode* mode);
|
||||
|
||||
extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||
CGSGlobalHotKeyOperatingMode mode);
|
||||
}
|
||||
#endif
|
||||
|
||||
#define MAX_GAMEPADS 4
|
||||
#define MAX_FINGERS 2
|
||||
|
||||
@@ -69,6 +84,8 @@ public:
|
||||
|
||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||
|
||||
void sendText(QString& string);
|
||||
|
||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||
|
||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||
@@ -81,6 +98,8 @@ public:
|
||||
|
||||
void notifyFocusLost();
|
||||
|
||||
void notifyFocusGained();
|
||||
|
||||
bool isCaptureActive();
|
||||
|
||||
bool isSystemKeyCaptureActive();
|
||||
@@ -93,10 +112,24 @@ public:
|
||||
|
||||
void flushMousePositionUpdate();
|
||||
|
||||
void updateKeyboardGrabState();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
private:
|
||||
enum KeyCombo {
|
||||
KeyComboQuit,
|
||||
KeyComboUngrabInput,
|
||||
KeyComboToggleFullScreen,
|
||||
KeyComboToggleStatsOverlay,
|
||||
KeyComboToggleMouseMode,
|
||||
KeyComboToggleCursorHide,
|
||||
KeyComboToggleMinimize,
|
||||
KeyComboPasteText,
|
||||
KeyComboMax
|
||||
};
|
||||
|
||||
GamepadState*
|
||||
findStateForGamepad(SDL_JoystickID id);
|
||||
|
||||
@@ -106,6 +139,8 @@ private:
|
||||
|
||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
void performSpecialKeyCombo(KeyCombo combo);
|
||||
|
||||
static
|
||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
@@ -124,10 +159,8 @@ private:
|
||||
static
|
||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
static
|
||||
LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
int clipboardThreadProc(void *ptr);
|
||||
|
||||
SDL_Window* m_Window;
|
||||
bool m_MultiController;
|
||||
@@ -154,7 +187,19 @@ private:
|
||||
bool m_FakeCaptureActive;
|
||||
QString m_OldIgnoreDevices;
|
||||
QString m_OldIgnoreDevicesExcept;
|
||||
bool m_CaptureSystemKeysEnabled;
|
||||
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
||||
int m_MouseCursorCapturedVisibilityState;
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSGlobalHotKeyOperatingMode m_OldHotKeyMode;
|
||||
#endif
|
||||
|
||||
struct {
|
||||
KeyCombo keyCombo;
|
||||
SDL_Keycode keyCode;
|
||||
SDL_Scancode scanCode;
|
||||
bool enabled;
|
||||
} m_SpecialKeyCombos[KeyComboMax];
|
||||
|
||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||
@@ -163,7 +208,6 @@ private:
|
||||
int m_StreamHeight;
|
||||
bool m_AbsoluteMouseMode;
|
||||
bool m_AbsoluteTouchMode;
|
||||
Uint32 m_PendingMouseLeaveButtonUp;
|
||||
|
||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||
@@ -172,5 +216,11 @@ private:
|
||||
char m_DragButton;
|
||||
int m_NumFingersDown;
|
||||
|
||||
SDL_Thread* m_ClipboardThread;
|
||||
SDL_atomic_t m_ShutdownClipboardThread;
|
||||
QString m_ClipboardData;
|
||||
SDL_cond* m_ClipboardHasData;
|
||||
SDL_mutex* m_ClipboardLock;
|
||||
|
||||
static const int k_ButtonMap[];
|
||||
};
|
||||
|
||||
+142
-168
@@ -3,8 +3,6 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
|
||||
@@ -15,13 +13,134 @@
|
||||
#define VK_NUMPAD0 0x60
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||
{
|
||||
switch (combo) {
|
||||
case KeyComboQuit:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
break;
|
||||
|
||||
case KeyComboUngrabInput:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleFullScreen:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleStatsOverlay:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
break;
|
||||
|
||||
case KeyComboToggleMouseMode:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
break;
|
||||
|
||||
case KeyComboToggleCursorHide:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState;
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyComboToggleMinimize:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected minimize combo");
|
||||
SDL_MinimizeWindow(m_Window);
|
||||
break;
|
||||
|
||||
case KeyComboPasteText:
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected type clipboard text combo");
|
||||
|
||||
// Force raise all keys to ensure that none of them interfere
|
||||
// with the text we're going to type.
|
||||
raiseAllKeys();
|
||||
|
||||
const char* text;
|
||||
if (SDL_HasClipboardText() && (text = SDL_GetClipboardText()) != nullptr) {
|
||||
// Append this data to the clipboard data string for the thread to process
|
||||
SDL_LockMutex(m_ClipboardLock);
|
||||
m_ClipboardData.append(text);
|
||||
SDL_CondSignal(m_ClipboardHasData);
|
||||
SDL_UnlockMutex(m_ClipboardLock);
|
||||
|
||||
// SDL_GetClipboardText() allocates, so we must free
|
||||
SDL_free((void*)text);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No text in clipboard to paste!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
#define IS_SYNTHETIC_KEY_EVENT(x) ((x)->timestamp == 0)
|
||||
|
||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
{
|
||||
short keyCode;
|
||||
char modifiers;
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for our special key combos
|
||||
if ((event->state == SDL_PRESSED) &&
|
||||
// Ignore timestamp == 0 which are sent from our keyboard code.
|
||||
if (!IS_SYNTHETIC_KEY_EVENT(event) &&
|
||||
(event->state == SDL_PRESSED) &&
|
||||
(event->keysym.mod & KMOD_CTRL) &&
|
||||
(event->keysym.mod & KMOD_ALT) &&
|
||||
(event->keysym.mod & KMOD_SHIFT)) {
|
||||
@@ -36,168 +155,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
// where the SDLK for one shortcut collides with
|
||||
// the scancode of another.
|
||||
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
if (event->keysym.sym == SDLK_q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (SDLK)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (SDLK)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (SDLK)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (SDLK)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
||||
else if (event->keysym.sym == SDLK_s) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (SDLK)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDLK_c) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (SDLK)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.sym == m_SpecialKeyCombos[i].keyCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.scancode == m_SpecialKeyCombos[i].scanCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (scancode)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (scancode)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (scancode)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (scancode)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (scancode)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDL_SCANCODE_C) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (scancode)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set modifier flags
|
||||
@@ -433,12 +403,16 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
// If this is a synthetic keypress from the clipboard code,
|
||||
// this will be on a non-main thread, so don't touch m_KeysDown.
|
||||
if (!IS_SYNTHETIC_KEY_EVENT(event)) {
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
|
||||
@@ -133,7 +133,15 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
||||
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||
if (buttonState == 0) {
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
if (m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
// Tell the main thread to stop capturing the mouse now
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = SDL_CODE_UNCAPTURE_MOUSE;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
}
|
||||
}
|
||||
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||
@@ -146,7 +154,8 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// executes.
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
event.user.code = (mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ?
|
||||
SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
if (!mouseInVideoRegion && buttonState != 0) {
|
||||
@@ -250,33 +259,5 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||
// Send mouse position updates if applicable
|
||||
me->flushMousePositionUpdate();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// See comment in SdlInputHandler::notifyMouseLeave()
|
||||
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
|
||||
int mouseX, mouseY;
|
||||
int windowX, windowY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
|
||||
|
||||
// If the button is now up, send the synthetic mouse up event
|
||||
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
|
||||
SDL_Event event;
|
||||
|
||||
event.button.type = SDL_MOUSEBUTTONUP;
|
||||
event.button.timestamp = SDL_GetTicks();
|
||||
event.button.windowID = SDL_GetWindowID(me->m_Window);
|
||||
event.button.which = 0;
|
||||
event.button.button = me->m_PendingMouseLeaveButtonUp;
|
||||
event.button.state = SDL_RELEASED;
|
||||
event.button.clicks = 1;
|
||||
event.button.x = mouseX - windowX;
|
||||
event.button.y = mouseY - windowY;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
me->m_PendingMouseLeaveButtonUp = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
+109
-62
@@ -37,14 +37,6 @@
|
||||
|
||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||
|
||||
// Running the connection process asynchronously seems to reliably
|
||||
// cause a crash in QSGRenderThread on Wayland and strange crashes
|
||||
// elsewhere. Until these are figured out, avoid the async connect
|
||||
// thread on Linux and BSDs.
|
||||
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
|
||||
#define USE_ASYNC_CONNECT_THREAD 1
|
||||
#endif
|
||||
|
||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clStageStarting,
|
||||
nullptr,
|
||||
@@ -65,11 +57,6 @@ void Session::clStageStarting(int stage)
|
||||
// which happens to be the main thread, so it's cool to interact
|
||||
// with the GUI in these callbacks.
|
||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clStageFailed(int stage, int errorCode)
|
||||
@@ -81,11 +68,6 @@ void Session::clStageFailed(int stage, int errorCode)
|
||||
char failingPorts[128];
|
||||
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clConnectionTerminated(int errorCode)
|
||||
@@ -199,6 +181,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
{
|
||||
DECODER_PARAMETERS params;
|
||||
|
||||
// We should never have vsync enabled for test-mode.
|
||||
// It introduces unnecessary delay for renderers that may
|
||||
// block while waiting for a backbuffer swap.
|
||||
SDL_assert(!enableVsync || !testOnly);
|
||||
|
||||
params.width = width;
|
||||
params.height = height;
|
||||
params.frameRate = frameRate;
|
||||
@@ -310,7 +297,7 @@ void Session::getDecoderInfo(SDL_Window* window,
|
||||
|
||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = isFullScreenOnly = false;
|
||||
return;
|
||||
}
|
||||
@@ -328,7 +315,7 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -350,7 +337,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -358,6 +345,10 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
|
||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||
|
||||
if (decoder->isAlwaysFullScreen()) {
|
||||
m_IsFullScreen = true;
|
||||
}
|
||||
|
||||
delete decoder;
|
||||
|
||||
return true;
|
||||
@@ -365,6 +356,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
|
||||
m_Computer(computer),
|
||||
m_App(app),
|
||||
m_Window(nullptr),
|
||||
@@ -441,6 +433,12 @@ bool Session::initialize()
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Enable audio encryption as long as we're not on Steam Link.
|
||||
// That hardware can hardly handle Opus decoding at all.
|
||||
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
|
||||
#endif
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Video bitrate: %d kbps",
|
||||
m_StreamConfig.bitrate);
|
||||
@@ -524,22 +522,17 @@ bool Session::initialize()
|
||||
{
|
||||
default:
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
// Only use full-screen desktop mode if we're running a desktop environment
|
||||
if (WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
}
|
||||
// Fall-through
|
||||
case StreamingPreferences::WM_FULLSCREEN:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// macOS behaves as if we're capturing system keys when
|
||||
// we enter "real" full-screen mode, so use standard
|
||||
// full-screen as our full-screen flag when capturing keys.
|
||||
if (m_Preferences->captureSysKeys) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 11)
|
||||
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
|
||||
// GUI environment. Force the session to use windowed mode (which won't
|
||||
@@ -581,10 +574,13 @@ void Session::emitLaunchWarning(QString text)
|
||||
// to allow it to transition off the screen before continuing.
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
||||
// Pump the UI loop while we wait
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
if (!m_ThreadedExec) {
|
||||
// Pump the UI loop while we wait if we're on the main thread
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +588,11 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
{
|
||||
QStringList warningList;
|
||||
|
||||
if (!m_Computer->isSupportedServerVersion) {
|
||||
emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
||||
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
||||
}
|
||||
@@ -835,7 +836,7 @@ void Session::getWindowDimensions(int& x, int& y,
|
||||
}
|
||||
}
|
||||
|
||||
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
||||
fullScreen = m_IsFullScreen;
|
||||
}
|
||||
|
||||
SDL_Rect usableBounds;
|
||||
@@ -944,10 +945,8 @@ void Session::toggleFullscreen()
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||
|
||||
if (fullScreen) {
|
||||
if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) {
|
||||
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
|
||||
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
|
||||
// We also need to grab if we're capturing system keys, because SDL requires window grab to
|
||||
// capture the keyboard on X11.
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
@@ -964,6 +963,9 @@ void Session::toggleFullscreen()
|
||||
// Reposition the window when the resize is complete
|
||||
m_PendingWindowedTransition = true;
|
||||
}
|
||||
|
||||
// Input handler might need to start/stop keyboard grab after changing modes
|
||||
m_InputHandler->updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void Session::notifyMouseEmulationMode(bool enabled)
|
||||
@@ -1002,17 +1004,7 @@ bool Session::startConnectionAsync()
|
||||
{
|
||||
// Wait 1.5 seconds before connecting to let the user
|
||||
// have time to read any messages present on the segue
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
SDL_Delay(1500);
|
||||
#else
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||
// Pump the UI loop while we wait since we're not async
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
// The UI should have ensured the old game was already quit
|
||||
// if we decide to stream a different game.
|
||||
@@ -1124,11 +1116,58 @@ void Session::flushWindowEvents()
|
||||
SDL_PushEvent(&flushEvent);
|
||||
}
|
||||
|
||||
class ExecThread : public QThread
|
||||
{
|
||||
public:
|
||||
ExecThread(Session* session) :
|
||||
QThread(nullptr),
|
||||
m_Session(session) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Session->execInternal();
|
||||
}
|
||||
|
||||
Session* m_Session;
|
||||
};
|
||||
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
|
||||
// Use a separate thread for the streaming session on X11 or Wayland
|
||||
// to ensure we don't stomp on Qt's GL context. This breaks when using
|
||||
// the Qt EGLFS backend, so we will restrict this to X11
|
||||
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
|
||||
|
||||
if (m_ThreadedExec) {
|
||||
// Run the streaming session on a separate thread for Linux/BSD
|
||||
ExecThread execThread(this);
|
||||
execThread.start();
|
||||
|
||||
// Until the SDL streaming window is created, we should continue
|
||||
// to update the Qt UI to allow warning messages to display and
|
||||
// make sure that the Qt window can hide itself.
|
||||
while (!execThread.wait(10) && m_Window == nullptr) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
// SDL is in charge now. Wait until the streaming thread exits
|
||||
// to further update the Qt window.
|
||||
execThread.wait();
|
||||
}
|
||||
else {
|
||||
// Run the streaming session on the main thread for Windows and macOS
|
||||
execInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void Session::execInternal()
|
||||
{
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
@@ -1152,22 +1191,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height);
|
||||
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
AsyncConnectionStartThread asyncConnThread(this);
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
if (!m_ThreadedExec) {
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#else
|
||||
asyncConnThread.run();
|
||||
#endif
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
else {
|
||||
// We're already in a separate thread so run the connection operations
|
||||
// synchronously and don't pump the event loop. The main thread is already
|
||||
// pumping the event loop for us.
|
||||
asyncConnThread.run();
|
||||
}
|
||||
|
||||
// If the connection failed, clean up and abort the connection.
|
||||
if (!m_AsyncConnectionSuccess) {
|
||||
@@ -1244,7 +1287,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
if (!m_IsFullScreen) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
// We must set the size before the position because centering
|
||||
@@ -1344,6 +1387,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
case SDL_CODE_SHOW_CURSOR:
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
break;
|
||||
case SDL_CODE_UNCAPTURE_MOUSE:
|
||||
SDL_CaptureMouse(SDL_FALSE);
|
||||
break;
|
||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||
m_FlushingWindowEventsRef--;
|
||||
break;
|
||||
@@ -1365,6 +1411,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
if (m_Preferences->muteOnFocusLoss) {
|
||||
m_AudioMuted = false;
|
||||
}
|
||||
m_InputHandler->notifyFocusGained();
|
||||
break;
|
||||
case SDL_WINDOWEVENT_LEAVE:
|
||||
m_InputHandler->notifyMouseLeave();
|
||||
|
||||
@@ -17,6 +17,7 @@ class Session : public QObject
|
||||
friend class SdlInputHandler;
|
||||
friend class DeferredSessionCleanupTask;
|
||||
friend class AsyncConnectionStartThread;
|
||||
friend class ExecThread;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
@@ -57,6 +58,8 @@ signals:
|
||||
void sessionFinished(int portTestResult);
|
||||
|
||||
private:
|
||||
void execInternal();
|
||||
|
||||
bool initialize();
|
||||
|
||||
bool startConnectionAsync();
|
||||
@@ -133,6 +136,7 @@ private:
|
||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
StreamingPreferences* m_Preferences;
|
||||
bool m_IsFullScreen;
|
||||
STREAM_CONFIGURATION m_StreamConfig;
|
||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
||||
@@ -147,6 +151,7 @@ private:
|
||||
Uint32 m_FullScreenFlag;
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
bool m_ThreadedExec;
|
||||
bool m_PendingWindowedTransition;
|
||||
bool m_UnexpectedTermination;
|
||||
SdlInputHandler* m_InputHandler;
|
||||
|
||||
@@ -14,12 +14,24 @@ extern "C" {
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
DrmRenderer::DrmRenderer()
|
||||
: m_DrmFd(-1),
|
||||
: m_HwContext(nullptr),
|
||||
m_DrmFd(-1),
|
||||
m_SdlOwnsDrmFd(false),
|
||||
m_SupportsDirectRendering(false),
|
||||
m_CrtcId(0),
|
||||
m_PlaneId(0),
|
||||
m_CurrentFbId(0)
|
||||
{
|
||||
#ifdef HAVE_EGL
|
||||
m_EGLExtDmaBuf = false;
|
||||
m_eglCreateImage = nullptr;
|
||||
m_eglCreateImageKHR = nullptr;
|
||||
m_eglDestroyImage = nullptr;
|
||||
m_eglDestroyImageKHR = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
DrmRenderer::~DrmRenderer()
|
||||
@@ -28,14 +40,22 @@ DrmRenderer::~DrmRenderer()
|
||||
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
||||
}
|
||||
|
||||
if (m_DrmFd != -1) {
|
||||
if (m_HwContext != nullptr) {
|
||||
av_buffer_unref(&m_HwContext);
|
||||
}
|
||||
|
||||
if (!m_SdlOwnsDrmFd && m_DrmFd != -1) {
|
||||
close(m_DrmFd);
|
||||
}
|
||||
}
|
||||
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
|
||||
{
|
||||
/* Nothing to do */
|
||||
// The out-of-tree LibreELEC patches use this option to control the type of the V4L2
|
||||
// buffers that we get back. We only support NV12 buffers now.
|
||||
av_dict_set_int(options, "pixel_format", AV_PIX_FMT_NV12, 0);
|
||||
|
||||
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using DRM renderer");
|
||||
@@ -43,33 +63,89 @@ bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
bool DrmRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
{
|
||||
const char* device = SDL_getenv("DRM_DEV");
|
||||
int i;
|
||||
|
||||
if (device == nullptr) {
|
||||
device = "/dev/dri/card0";
|
||||
}
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Opening DRM device: %s",
|
||||
device);
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||
if (m_DrmFd < 0) {
|
||||
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open DRM device: %d",
|
||||
errno);
|
||||
"SDL_GetWindowWMInfo() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.subsystem == SDL_SYSWM_KMSDRM) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Sharing DRM FD with SDL");
|
||||
|
||||
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
|
||||
m_DrmFd = info.info.kmsdrm.drm_fd;
|
||||
m_SdlOwnsDrmFd = true;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
const char* device = SDL_getenv("DRM_DEV");
|
||||
|
||||
if (device == nullptr) {
|
||||
device = "/dev/dri/card0";
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Opening DRM device: %s",
|
||||
device);
|
||||
|
||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||
if (m_DrmFd < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open DRM device: %d",
|
||||
errno);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the device context first because it is needed whether we can
|
||||
// actually use direct rendering or not.
|
||||
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_DRM);
|
||||
if (m_HwContext == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"av_hwdevice_ctx_alloc(DRM) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||
AVDRMDeviceContext* drmDeviceContext = (AVDRMDeviceContext*)deviceContext->hwctx;
|
||||
|
||||
drmDeviceContext->fd = m_DrmFd;
|
||||
|
||||
int err = av_hwdevice_ctx_init(m_HwContext);
|
||||
if (err < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"av_hwdevice_ctx_init(DRM) failed: %d",
|
||||
err);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
// Still return true if we fail to initialize DRM direct rendering
|
||||
// stuff, since we have EGL that we can use for indirect rendering.
|
||||
const bool DIRECT_RENDERING_INIT_FAILED = true;
|
||||
#else
|
||||
// Fail if we can't initialize direct rendering and we don't have EGL.
|
||||
const bool DIRECT_RENDERING_INIT_FAILED = false;
|
||||
#endif
|
||||
|
||||
drmModeRes* resources = drmModeGetResources(m_DrmFd);
|
||||
if (resources == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmModeGetResources() failed: %d",
|
||||
errno);
|
||||
return false;
|
||||
"drmModeGetResources() failed: %d",
|
||||
errno);
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Look for a connected connector and get the associated encoder
|
||||
@@ -89,7 +165,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No connected displays found!");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Now find the CRTC from the encoder
|
||||
@@ -109,7 +185,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM encoder not found!");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
int crtcIndex = -1;
|
||||
@@ -130,7 +206,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
if (crtcIndex == -1) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to get CRTC!");
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
||||
@@ -140,7 +216,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmGetPlaneResources() failed: %d",
|
||||
errno);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Find an NV12 overlay plane to render on
|
||||
@@ -185,6 +261,15 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
|
||||
drmModeFreePlaneResources(planeRes);
|
||||
|
||||
if (m_PlaneId == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to find suitable NV12 overlay plane!");
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// If we got this far, we can do direct rendering via the DRM FD.
|
||||
m_SupportsDirectRendering = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -213,6 +298,8 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
uint32_t handles[4] = {};
|
||||
uint32_t pitches[4] = {};
|
||||
uint32_t offsets[4] = {};
|
||||
uint64_t modifiers[4] = {};
|
||||
uint32_t flags = 0;
|
||||
|
||||
SDL_Rect src, dst;
|
||||
|
||||
@@ -234,12 +321,18 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass along the modifiers to DRM if there are some in the descriptor
|
||||
if (drmFrame->objects[0].format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
flags |= DRM_MODE_FB_MODIFIERS;
|
||||
}
|
||||
|
||||
SDL_assert(drmFrame->nb_layers == 1);
|
||||
SDL_assert(drmFrame->layers[0].nb_planes == 2);
|
||||
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
|
||||
handles[i] = primeHandle;
|
||||
pitches[i] = drmFrame->layers[0].planes[i].pitch;
|
||||
offsets[i] = drmFrame->layers[0].planes[i].offset;
|
||||
modifiers[i] = drmFrame->objects[0].format_modifier;
|
||||
}
|
||||
|
||||
// Remember the last FB object we created so we can free it
|
||||
@@ -247,12 +340,15 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
uint32_t lastFbId = m_CurrentFbId;
|
||||
|
||||
// Create a frame buffer object from the PRIME buffer
|
||||
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
|
||||
drmFrame->layers[0].format,
|
||||
handles, pitches, offsets, &m_CurrentFbId, 0);
|
||||
// NB: It is an error to pass modifiers without DRM_MODE_FB_MODIFIERS set.
|
||||
err = drmModeAddFB2WithModifiers(m_DrmFd, frame->width, frame->height,
|
||||
drmFrame->layers[0].format,
|
||||
handles, pitches, offsets,
|
||||
(flags & DRM_MODE_FB_MODIFIERS) ? modifiers : NULL,
|
||||
&m_CurrentFbId, flags);
|
||||
if (err < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmModeAddFB2() failed: %d",
|
||||
"drmModeAddFB2WithModifiers() failed: %d",
|
||||
errno);
|
||||
m_CurrentFbId = lastFbId;
|
||||
return;
|
||||
@@ -277,3 +373,200 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
// Free the previous FB object which has now been superseded
|
||||
drmModeRmFB(m_DrmFd, lastFbId);
|
||||
}
|
||||
|
||||
bool DrmRenderer::needsTestFrame()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DrmRenderer::isDirectRenderingSupported()
|
||||
{
|
||||
return m_SupportsDirectRendering;
|
||||
}
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
|
||||
bool DrmRenderer::canExportEGL() {
|
||||
if (qgetenv("DRM_FORCE_DIRECT") == "1") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using direct rendering due to environment variable");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM backend supports exporting EGLImage");
|
||||
return true;
|
||||
}
|
||||
|
||||
AVPixelFormat DrmRenderer::getEGLImagePixelFormat() {
|
||||
// This tells EGLRenderer to treat the EGLImage as a single opaque texture
|
||||
return AV_PIX_FMT_DRM_PRIME;
|
||||
}
|
||||
|
||||
bool DrmRenderer::initializeEGL(EGLDisplay,
|
||||
const EGLExtensions &ext) {
|
||||
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM-EGL: DMABUF unsupported");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||
|
||||
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
|
||||
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
|
||||
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
|
||||
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
|
||||
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
|
||||
|
||||
if (!(m_eglCreateImage && m_eglDestroyImage) &&
|
||||
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ssize_t DrmRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
||||
EGLImage images[EGL_MAX_PLANES]) {
|
||||
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||
|
||||
memset(images, 0, sizeof(EGLImage) * EGL_MAX_PLANES);
|
||||
|
||||
SDL_assert(drmFrame->nb_objects == 1);
|
||||
SDL_assert(drmFrame->nb_layers == 1);
|
||||
|
||||
// Max 30 attributes (1 key + 1 value for each)
|
||||
const int MAX_ATTRIB_COUNT = 30 * 2;
|
||||
EGLAttrib attribs[MAX_ATTRIB_COUNT] = {
|
||||
EGL_LINUX_DRM_FOURCC_EXT, (EGLAttrib)drmFrame->layers[0].format,
|
||||
EGL_WIDTH, frame->width,
|
||||
EGL_HEIGHT, frame->height,
|
||||
};
|
||||
int attribIndex = 6;
|
||||
|
||||
for (int i = 0; i < drmFrame->layers[0].nb_planes; ++i) {
|
||||
const auto &plane = drmFrame->layers[0].planes[i];
|
||||
const auto &object = drmFrame->objects[plane.object_index];
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate the attribute list
|
||||
attribs[attribIndex++] = EGL_NONE;
|
||||
SDL_assert(attribIndex <= MAX_ATTRIB_COUNT);
|
||||
|
||||
// Our EGLImages are non-planar, so we only populate the first entry
|
||||
if (m_eglCreateImage) {
|
||||
images[0] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, attribs);
|
||||
if (!images[0]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImage() Failed: %d", eglGetError());
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Cast the EGLAttrib array elements to EGLint for the KHR extension
|
||||
EGLint intAttribs[MAX_ATTRIB_COUNT];
|
||||
for (int i = 0; i < MAX_ATTRIB_COUNT; i++) {
|
||||
intAttribs[i] = (EGLint)attribs[i];
|
||||
}
|
||||
|
||||
images[0] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, intAttribs);
|
||||
if (!images[0]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImageKHR() Failed: %d", eglGetError());
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
freeEGLImages(dpy, images);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DrmRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||
if (m_eglDestroyImage) {
|
||||
m_eglDestroyImage(dpy, images[0]);
|
||||
}
|
||||
else {
|
||||
m_eglDestroyImageKHR(dpy, images[0]);
|
||||
}
|
||||
|
||||
// Our EGLImages are non-planar
|
||||
SDL_assert(images[1] == 0);
|
||||
SDL_assert(images[2] == 0);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,12 +14,32 @@ public:
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
virtual int getRendererAttributes() override;
|
||||
virtual bool needsTestFrame() override;
|
||||
virtual bool isDirectRenderingSupported() override;
|
||||
#ifdef HAVE_EGL
|
||||
virtual bool canExportEGL() override;
|
||||
virtual AVPixelFormat getEGLImagePixelFormat() override;
|
||||
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
AVBufferRef* m_HwContext;
|
||||
int m_DrmFd;
|
||||
bool m_SdlOwnsDrmFd;
|
||||
bool m_SupportsDirectRendering;
|
||||
uint32_t m_CrtcId;
|
||||
uint32_t m_PlaneId;
|
||||
uint32_t m_CurrentFbId;
|
||||
SDL_Rect m_OutputRect;
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
bool m_EGLExtDmaBuf;
|
||||
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
|
||||
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
|
||||
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
|
||||
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
||||
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
||||
if (!EGLExtensionsStr) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions: %d", eglGetError());
|
||||
return QStringList();
|
||||
}
|
||||
return QString(EGLExtensionsStr).split(" ");
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
#include <SDL_render.h>
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
#ifndef EGL_VERSION_1_5
|
||||
typedef intptr_t EGLAttrib;
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
#endif
|
||||
|
||||
#ifndef EGL_EXT_platform_base
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||
#endif
|
||||
|
||||
// These are EGL extensions, so some platform headers may not provide them
|
||||
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
||||
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
||||
@@ -34,6 +22,9 @@ typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platf
|
||||
#ifndef EGL_PLATFORM_X11_KHR
|
||||
#define EGL_PLATFORM_X11_KHR 0x31D5
|
||||
#endif
|
||||
#ifndef EGL_PLATFORM_GBM_KHR
|
||||
#define EGL_PLATFORM_GBM_KHR 0x31D7
|
||||
#endif
|
||||
|
||||
typedef struct _OVERLAY_VERTEX
|
||||
{
|
||||
@@ -68,7 +59,7 @@ SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
|
||||
|
||||
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||
:
|
||||
m_SwPixelFormat(AV_PIX_FMT_NONE),
|
||||
m_EGLImagePixelFormat(AV_PIX_FMT_NONE),
|
||||
m_EGLDisplay(EGL_NO_DISPLAY),
|
||||
m_Textures{0},
|
||||
m_OverlayTextures{0},
|
||||
@@ -83,6 +74,7 @@ EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||
m_ColorSpace(AVCOL_SPC_NB),
|
||||
m_ColorFull(false),
|
||||
m_BlockingSwapBuffers(false),
|
||||
m_LastFrame(av_frame_alloc()),
|
||||
m_glEGLImageTargetTexture2DOES(nullptr),
|
||||
m_glGenVertexArraysOES(nullptr),
|
||||
m_glBindVertexArrayOES(nullptr),
|
||||
@@ -133,6 +125,8 @@ EGLRenderer::~EGLRenderer()
|
||||
SDL_DestroyRenderer(m_DummyRenderer);
|
||||
}
|
||||
|
||||
av_frame_free(&m_LastFrame);
|
||||
|
||||
// Reset the global properties back to what they were before
|
||||
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
||||
@@ -162,16 +156,16 @@ void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||
}
|
||||
}
|
||||
|
||||
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
|
||||
bool EGLRenderer::isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat)
|
||||
{
|
||||
// Remember to keep this in sync with EGLRenderer::renderFrame()!
|
||||
switch (pixelFormat)
|
||||
{
|
||||
case AV_PIX_FMT_NV12:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
// Pixel format support should be determined by the backend renderer
|
||||
return m_Backend->isPixelFormatSupported(videoFormat, pixelFormat);
|
||||
}
|
||||
|
||||
AVPixelFormat EGLRenderer::getPreferredPixelFormat(int videoFormat)
|
||||
{
|
||||
// Pixel format preference should be determined by the backend renderer
|
||||
return m_Backend->getPreferredPixelFormat(videoFormat);
|
||||
}
|
||||
|
||||
void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
@@ -195,7 +189,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} overlayRect;
|
||||
} overlayRect = {};
|
||||
|
||||
// These overlay positions differ from the other renderers because OpenGL
|
||||
// places the origin in the lower-left corner instead of the upper-left.
|
||||
@@ -208,6 +202,8 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
// Top left
|
||||
overlayRect.x = 0;
|
||||
overlayRect.y = m_ViewportHeight - newSurface->h;
|
||||
} else {
|
||||
SDL_assert(false);
|
||||
}
|
||||
|
||||
overlayRect.w = newSurface->w;
|
||||
@@ -254,7 +250,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||
glUniform1i(m_OverlayShaderProgramParams[PARAM_TEXTURE], 0);
|
||||
glUniform1i(m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE], 0);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
@@ -374,27 +370,42 @@ bool EGLRenderer::compileShaders() {
|
||||
SDL_assert(!m_ShaderProgram);
|
||||
SDL_assert(!m_OverlayShaderProgram);
|
||||
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
// XXX: TODO: other formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||
m_ShaderProgram = compileShader("egl_nv12.vert", "egl_nv12.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgram = compileShader("egl.vert", "egl.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
m_ShaderProgramParams[NV12_PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||
m_ShaderProgramParams[NV12_PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||
m_ShaderProgramParams[NV12_PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||
m_ShaderProgramParams[NV12_PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||
}
|
||||
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||
m_ShaderProgram = compileShader("egl_opaque.vert", "egl_opaque.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE] = glGetUniformLocation(m_ShaderProgram, "uTexture");
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unsupported EGL pixel format: %d",
|
||||
m_EGLImagePixelFormat);
|
||||
SDL_assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgramParams[PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||
m_ShaderProgramParams[PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||
m_ShaderProgramParams[PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||
m_ShaderProgramParams[PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||
|
||||
m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag");
|
||||
if (!m_OverlayShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OverlayShaderProgramParams[PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
||||
m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -479,6 +490,13 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||
case SDL_SYSWM_KMSDRM:
|
||||
if (!openDisplay(EGL_PLATFORM_GBM_KHR, info.info.kmsdrm.gbm_dev)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
EGL_LOG(Error, "not compatible with SYSWM");
|
||||
@@ -544,7 +562,7 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
src.x = src.y = dst.x = dst.y = 0;
|
||||
src.w = params->width;
|
||||
src.h = params->height;
|
||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||
SDL_GL_GetDrawableSize(m_Window, &dst.w, &dst.h);
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||
@@ -557,7 +575,17 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
|
||||
if (params->enableVsync) {
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
m_BlockingSwapBuffers = true;
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||
// The SDL KMSDRM backend already enforces double buffering (due to
|
||||
// SDL_HINT_VIDEO_DOUBLE_BUFFER=1), so calling glFinish() after
|
||||
// SDL_GL_SwapWindow() will block an extra frame and lock rendering
|
||||
// at 1/2 the display refresh rate.
|
||||
if (info.subsystem != SDL_SYSWM_KMSDRM)
|
||||
#endif
|
||||
{
|
||||
m_BlockingSwapBuffers = true;
|
||||
}
|
||||
} else {
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
}
|
||||
@@ -648,7 +676,18 @@ const float *EGLRenderer::getColorMatrix() {
|
||||
case AVCOL_SPC_BT2020_CL:
|
||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
// Some backends don't populate this, so we'll assume
|
||||
// the host gave us what we asked for by default.
|
||||
switch (getDecoderColorspace()) {
|
||||
case COLORSPACE_REC_601:
|
||||
return m_ColorFull ? bt601Full : bt601Lim;
|
||||
case COLORSPACE_REC_709:
|
||||
return m_ColorFull ? bt709Full : bt709Lim;
|
||||
case COLORSPACE_REC_2020:
|
||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
}
|
||||
};
|
||||
|
||||
return bt601Lim;
|
||||
@@ -721,49 +760,49 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr) {
|
||||
// Find the native read-back format and load the shader
|
||||
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
|
||||
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||
// Find the native read-back format and load the shaders
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NONE) {
|
||||
m_EGLImagePixelFormat = m_Backend->getEGLImagePixelFormat();
|
||||
EGL_LOG(Info, "EGLImage pixel format: %d", m_EGLImagePixelFormat);
|
||||
|
||||
m_SwPixelFormat = hwFrameCtx->sw_format;
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
|
||||
m_ColorSpace = frame->colorspace;
|
||||
|
||||
// XXX: TODO: Handle other pixel formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
m_ColorSpace = frame->colorspace;
|
||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||
// This handles the case where the color range is unknown,
|
||||
// so that we use Limited color range which is the default
|
||||
// behavior for Moonlight.
|
||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||
|
||||
if (!specialize()) {
|
||||
m_SwPixelFormat = AV_PIX_FMT_NONE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count < 0)
|
||||
if (!specialize()) {
|
||||
m_EGLImagePixelFormat = AV_PIX_FMT_NONE;
|
||||
return;
|
||||
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||
}
|
||||
} else {
|
||||
// TODO: load texture for SW decoding ?
|
||||
EGL_LOG(Error, "EGL rendering only supports hw frames");
|
||||
}
|
||||
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count < 0)
|
||||
return;
|
||||
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||
}
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_ShaderProgram);
|
||||
m_glBindVertexArrayOES(m_VAO);
|
||||
|
||||
glUniformMatrix3fv(m_ShaderProgramParams[PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
||||
glUniform3fv(m_ShaderProgramParams[PARAM_OFFSET], 1, getColorOffsets());
|
||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE1], 0);
|
||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE2], 1);
|
||||
// Bind parameters for the shaders
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||
glUniformMatrix3fv(m_ShaderProgramParams[NV12_PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
||||
glUniform3fv(m_ShaderProgramParams[NV12_PARAM_OFFSET], 1, getColorOffsets());
|
||||
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE1], 0);
|
||||
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE2], 1);
|
||||
}
|
||||
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||
glUniform1i(m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE], 0);
|
||||
}
|
||||
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
|
||||
@@ -787,6 +826,31 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
||||
glFinish();
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr)
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
|
||||
// Free the DMA-BUF backing the last frame now that it is definitely
|
||||
// no longer being used anymore. While the PRIME FD stays around until
|
||||
// EGL is done with it, the memory backing it may be reused by FFmpeg
|
||||
// before the GPU has read it. This is particularly noticeable on the
|
||||
// RK3288-based TinkerBoard when V-Sync is disabled.
|
||||
av_frame_unref(m_LastFrame);
|
||||
av_frame_move_ref(m_LastFrame, frame);
|
||||
}
|
||||
|
||||
bool EGLRenderer::testRenderFrame(AVFrame* frame)
|
||||
{
|
||||
EGLImage imgs[EGL_MAX_PLANES];
|
||||
|
||||
// Make sure we can get working EGLImages from the backend renderer.
|
||||
// Some devices (Raspberry Pi) will happily decode into DRM formats that
|
||||
// its own GL implementation won't accept in eglCreateImage().
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count <= 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Backend failed to export EGL image for test frame");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ public:
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual bool testRenderFrame(AVFrame* frame) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -29,7 +31,7 @@ private:
|
||||
int m_ViewportWidth;
|
||||
int m_ViewportHeight;
|
||||
|
||||
int m_SwPixelFormat;
|
||||
AVPixelFormat m_EGLImagePixelFormat;
|
||||
void *m_EGLDisplay;
|
||||
unsigned m_Textures[EGL_MAX_PLANES];
|
||||
unsigned m_OverlayTextures[Overlay::OverlayMax];
|
||||
@@ -44,18 +46,20 @@ private:
|
||||
int m_ColorSpace;
|
||||
bool m_ColorFull;
|
||||
bool m_BlockingSwapBuffers;
|
||||
AVFrame* m_LastFrame;
|
||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
||||
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
||||
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
||||
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
||||
|
||||
#define PARAM_YUVMAT 0
|
||||
#define PARAM_OFFSET 1
|
||||
#define PARAM_PLANE1 2
|
||||
#define PARAM_PLANE2 3
|
||||
#define NV12_PARAM_YUVMAT 0
|
||||
#define NV12_PARAM_OFFSET 1
|
||||
#define NV12_PARAM_PLANE1 2
|
||||
#define NV12_PARAM_PLANE2 3
|
||||
#define OPAQUE_PARAM_TEXTURE 0
|
||||
int m_ShaderProgramParams[4];
|
||||
|
||||
#define PARAM_TEXTURE 0
|
||||
#define OVERLAY_PARAM_TEXTURE 0
|
||||
int m_OverlayShaderProgramParams[1];
|
||||
|
||||
int m_OldContextProfileMask;
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
MmalRenderer::MmalRenderer()
|
||||
: m_Renderer(nullptr),
|
||||
m_InputPort(nullptr),
|
||||
m_BackgroundRenderer(nullptr)
|
||||
m_BackgroundRenderer(nullptr),
|
||||
m_Window(nullptr),
|
||||
m_VideoWidth(0),
|
||||
m_VideoHeight(0),
|
||||
m_LastWindowPosX(-1),
|
||||
m_LastWindowPosY(-1)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -46,10 +51,67 @@ bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**
|
||||
return true;
|
||||
}
|
||||
|
||||
void MmalRenderer::updateDisplayRegion()
|
||||
{
|
||||
MMAL_STATUS_T status;
|
||||
int currentPosX, currentPosY;
|
||||
MMAL_DISPLAYREGION_T dr;
|
||||
|
||||
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
|
||||
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
|
||||
dr.set = MMAL_DISPLAY_SET_DEST_RECT;
|
||||
|
||||
SDL_GetWindowPosition(m_Window, ¤tPosX, ¤tPosY);
|
||||
|
||||
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_INPUT_FOCUS) == 0) {
|
||||
dr.dest_rect.x = 0;
|
||||
dr.dest_rect.y = 0;
|
||||
dr.dest_rect.width = 0;
|
||||
dr.dest_rect.height = 0;
|
||||
|
||||
// Force a re-evaluation next time
|
||||
m_LastWindowPosX = -1;
|
||||
m_LastWindowPosY = -1;
|
||||
}
|
||||
else if (m_LastWindowPosX != currentPosX || m_LastWindowPosY != currentPosY) {
|
||||
SDL_Rect src, dst;
|
||||
src.x = src.y = 0;
|
||||
src.w = m_VideoWidth;
|
||||
src.h = m_VideoHeight;
|
||||
dst.x = dst.y = 0;
|
||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
dr.dest_rect.x = currentPosX + dst.x;
|
||||
dr.dest_rect.y = currentPosY + dst.y;
|
||||
dr.dest_rect.width = dst.w;
|
||||
dr.dest_rect.height = dst.h;
|
||||
|
||||
m_LastWindowPosX = currentPosX;
|
||||
m_LastWindowPosY = currentPosY;
|
||||
}
|
||||
else {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
||||
if (status != MMAL_SUCCESS) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"mmal_port_parameter_set() failed: %x (%s)",
|
||||
status, mmal_status_to_string(status));
|
||||
}
|
||||
}
|
||||
|
||||
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
{
|
||||
MMAL_STATUS_T status;
|
||||
|
||||
m_Window = params->window;
|
||||
m_VideoWidth = params->width;
|
||||
m_VideoHeight = params->height;
|
||||
|
||||
// Clear the background if possible
|
||||
setupBackground(params);
|
||||
|
||||
@@ -107,23 +169,6 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
dr.src_rect.width = params->width;
|
||||
dr.src_rect.height = params->height;
|
||||
|
||||
{
|
||||
SDL_Rect src, dst;
|
||||
src.x = src.y = 0;
|
||||
src.w = params->width;
|
||||
src.h = params->height;
|
||||
dst.x = dst.y = 0;
|
||||
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
|
||||
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
|
||||
dr.dest_rect.x = dst.x;
|
||||
dr.dest_rect.y = dst.y;
|
||||
dr.dest_rect.width = dst.w;
|
||||
dr.dest_rect.height = dst.h;
|
||||
}
|
||||
|
||||
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
||||
if (status != MMAL_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -131,6 +176,9 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
status, mmal_status_to_string(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set the destination display region
|
||||
updateDisplayRegion();
|
||||
}
|
||||
|
||||
status = mmal_port_enable(m_InputPort, InputPortCallback);
|
||||
@@ -179,17 +227,16 @@ void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
|
||||
mmal_buffer_header_release(buffer);
|
||||
}
|
||||
|
||||
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
|
||||
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int)
|
||||
{
|
||||
// Opaque MMAL buffers
|
||||
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
|
||||
return AV_PIX_FMT_MMAL;
|
||||
}
|
||||
|
||||
int MmalRenderer::getRendererAttributes()
|
||||
{
|
||||
// This renderer can only draw in full-screen and maxes out at 1080p
|
||||
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX;
|
||||
// This renderer maxes out at 1080p
|
||||
return RENDERER_ATTRIBUTE_1080P_MAX;
|
||||
}
|
||||
|
||||
bool MmalRenderer::needsTestFrame()
|
||||
@@ -209,6 +256,9 @@ void MmalRenderer::renderFrame(AVFrame* frame)
|
||||
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
||||
MMAL_STATUS_T status;
|
||||
|
||||
// Update the destination display region in case the window moved
|
||||
updateDisplayRegion();
|
||||
|
||||
status = mmal_port_send_buffer(m_InputPort, buffer);
|
||||
if (status != MMAL_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
||||
@@ -22,10 +22,14 @@ private:
|
||||
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
|
||||
|
||||
void setupBackground(PDECODER_PARAMETERS params);
|
||||
void updateDisplayRegion();
|
||||
|
||||
MMAL_COMPONENT_T* m_Renderer;
|
||||
MMAL_PORT_T* m_InputPort;
|
||||
|
||||
SDL_Renderer* m_BackgroundRenderer;
|
||||
SDL_Window* m_Window;
|
||||
int m_VideoWidth, m_VideoHeight;
|
||||
int m_LastWindowPosX, m_LastWindowPosY;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,9 +10,60 @@ extern "C" {
|
||||
}
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
// SDL_egl.h have too many conflicts, we will do without
|
||||
typedef void *EGLDisplay;
|
||||
#define MESA_EGL_NO_X11_HEADERS
|
||||
#define EGL_NO_X11
|
||||
#include <SDL_egl.h>
|
||||
|
||||
#ifndef EGL_VERSION_1_5
|
||||
typedef intptr_t EGLAttrib;
|
||||
typedef void *EGLImage;
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEPROC) (EGLDisplay dpy, EGLImage image);
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_KHR_image) || !defined(EGL_EGLEXT_PROTOTYPES)
|
||||
// EGL_KHR_image technically uses EGLImageKHR instead of EGLImage, but they're compatible
|
||||
// so we swap them here to avoid mixing them all over the place
|
||||
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
|
||||
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC) (EGLDisplay dpy, EGLImage image);
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_EXT_platform_base) || !defined(EGL_EGLEXT_PROTOTYPES)
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||
#endif
|
||||
|
||||
#ifndef EGL_EXT_image_dma_buf_import
|
||||
#define EGL_LINUX_DMA_BUF_EXT 0x3270
|
||||
#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
|
||||
#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
|
||||
#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
|
||||
#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
|
||||
#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
|
||||
#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
|
||||
#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
|
||||
#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
|
||||
#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
|
||||
#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
|
||||
#endif
|
||||
|
||||
#ifndef EGL_EXT_image_dma_buf_import_modifiers
|
||||
#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
|
||||
#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
|
||||
#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
|
||||
#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
|
||||
#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
|
||||
#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
|
||||
#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
|
||||
#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
|
||||
#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
|
||||
#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
|
||||
#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
|
||||
#endif
|
||||
|
||||
#define EGL_MAX_PLANES 4
|
||||
|
||||
class EGLExtensions {
|
||||
@@ -35,6 +86,13 @@ public:
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
|
||||
virtual void renderFrame(AVFrame* frame) = 0;
|
||||
|
||||
virtual bool testRenderFrame(AVFrame*) {
|
||||
// If the renderer doesn't provide an explicit test routine,
|
||||
// we will always assume that any returned AVFrame can be
|
||||
// rendered successfully.
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool needsTestFrame() {
|
||||
// No test frame required by default
|
||||
return false;
|
||||
@@ -65,7 +123,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) {
|
||||
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) {
|
||||
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||
// 10-bit YUV 4:2:0
|
||||
return AV_PIX_FMT_P010;
|
||||
@@ -76,7 +134,7 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) {
|
||||
virtual bool isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat) {
|
||||
// By default, we only support the preferred pixel format
|
||||
return getPreferredPixelFormat(videoFormat) == pixelFormat;
|
||||
}
|
||||
@@ -92,6 +150,10 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual AVPixelFormat getEGLImagePixelFormat() {
|
||||
return AV_PIX_FMT_NONE;
|
||||
}
|
||||
|
||||
virtual bool initializeEGL(EGLDisplay,
|
||||
const EGLExtensions &) {
|
||||
return false;
|
||||
|
||||
@@ -351,3 +351,36 @@ Exit:
|
||||
av_frame_free(&swFrame);
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlRenderer::testRenderFrame(AVFrame* frame)
|
||||
{
|
||||
// If we are acting as the frontend for a hardware
|
||||
// accelerated decoder, we'll need to read the frame
|
||||
// back to render it. Test that this can be done
|
||||
// for the given frame successfully.
|
||||
if (frame->hw_frames_ctx != nullptr) {
|
||||
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||
|
||||
AVFrame* swFrame = av_frame_alloc();
|
||||
if (swFrame == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
swFrame->width = frame->width;
|
||||
swFrame->height = frame->height;
|
||||
swFrame->format = hwFrameCtx->sw_format;
|
||||
|
||||
int err = av_hwframe_transfer_data(swFrame, frame, 0);
|
||||
|
||||
av_frame_free(&swFrame);
|
||||
|
||||
if (err != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"av_hwframe_transfer_data() failed: %d",
|
||||
err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public:
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual bool isRenderThreadSupported() override;
|
||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||
virtual bool testRenderFrame(AVFrame* frame) override;
|
||||
|
||||
private:
|
||||
void renderOverlay(Overlay::OverlayType type);
|
||||
|
||||
@@ -5,22 +5,23 @@
|
||||
#include <streaming/streamutils.h>
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
#ifdef HAVE_EGL
|
||||
#include <SDL_egl.h>
|
||||
#endif
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
VAAPIRenderer::VAAPIRenderer()
|
||||
: m_HwContext(nullptr),
|
||||
m_DrmFd(-1),
|
||||
m_BlacklistedForDirectRendering(false)
|
||||
{
|
||||
#ifdef HAVE_EGL
|
||||
m_PrimeDescriptor.num_layers = 0;
|
||||
m_PrimeDescriptor.num_objects = 0;
|
||||
m_EGLExtDmaBuf = false;
|
||||
|
||||
m_eglCreateImage = nullptr;
|
||||
m_eglCreateImageKHR = nullptr;
|
||||
m_eglDestroyImage = nullptr;
|
||||
m_eglDestroyImageKHR = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -39,10 +40,6 @@ VAAPIRenderer::~VAAPIRenderer()
|
||||
vaTerminate(display);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_DrmFd != -1) {
|
||||
close(m_DrmFd);
|
||||
}
|
||||
}
|
||||
|
||||
VADisplay
|
||||
@@ -90,41 +87,17 @@ VAAPIRenderer::openDisplay(SDL_Window* window)
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
|
||||
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
|
||||
#ifdef HAVE_LIBVA_DRM
|
||||
if (m_DrmFd < 0) {
|
||||
const char* device = SDL_getenv("DRM_DEV");
|
||||
|
||||
if (device == nullptr) {
|
||||
device = "/dev/dri/card0";
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Opening DRM device: %s",
|
||||
device);
|
||||
|
||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||
if (m_DrmFd < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open DRM device: %d",
|
||||
errno);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
display = vaGetDisplayDRM(m_DrmFd);
|
||||
#if defined(SDL_VIDEO_DRIVER_KMSDRM) && defined(HAVE_LIBVA_DRM) && SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
else if (info.subsystem == SDL_SYSWM_KMSDRM) {
|
||||
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
|
||||
display = vaGetDisplayDRM(info.info.kmsdrm.drm_fd);
|
||||
if (display == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to open DRM display for VAAPI");
|
||||
return nullptr;
|
||||
}
|
||||
#else
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Moonlight not compiled with VAAPI DRM support!");
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unsupported VAAPI rendering subsystem: %d",
|
||||
@@ -259,12 +232,12 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
"Driver: %s",
|
||||
vendorString ? vendorString : "<unknown>");
|
||||
|
||||
// AMD's Gallium VAAPI driver has a nasty memory leak that causes memory
|
||||
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU
|
||||
// driver in place that works well, so use that instead on AMD systems. If
|
||||
// we're using Wayland, we have no choice but to use VAAPI because VDPAU
|
||||
// Older versions of the Gallium VAAPI driver have a nasty memory leak that
|
||||
// causes memory to be leaked for each submitted frame. I believe this is
|
||||
// resolved in the libva2 drivers (VAAPI 1.x).
|
||||
// If we're using Wayland, we have no choice but to use VAAPI because VDPAU
|
||||
// is only supported under X11 or XWayland.
|
||||
if (qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
|
||||
if (major == 0 && qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
|
||||
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
|
||||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
|
||||
// Fail and let VDPAU pick this up
|
||||
@@ -510,6 +483,10 @@ VAAPIRenderer::canExportEGL() {
|
||||
return true;
|
||||
}
|
||||
|
||||
AVPixelFormat VAAPIRenderer::getEGLImagePixelFormat() {
|
||||
return AV_PIX_FMT_NV12;
|
||||
}
|
||||
|
||||
bool
|
||||
VAAPIRenderer::initializeEGL(EGLDisplay,
|
||||
const EGLExtensions &ext) {
|
||||
@@ -519,6 +496,20 @@ VAAPIRenderer::initializeEGL(EGLDisplay,
|
||||
return false;
|
||||
}
|
||||
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||
|
||||
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
|
||||
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
|
||||
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
|
||||
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
|
||||
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
|
||||
|
||||
if (!(m_eglCreateImage && m_eglDestroyImage) &&
|
||||
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -554,33 +545,54 @@ VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
||||
const auto &layer = m_PrimeDescriptor.layers[i];
|
||||
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
|
||||
|
||||
EGLAttrib attribs[17] = {
|
||||
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
|
||||
const int EGL_ATTRIB_COUNT = 17;
|
||||
EGLAttrib attribs[EGL_ATTRIB_COUNT] = {
|
||||
EGL_LINUX_DRM_FOURCC_EXT, layer.drm_format,
|
||||
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
|
||||
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
|
||||
EGL_NONE,
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, layer.offset[0],
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, layer.pitch[0],
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (EGLint)object.drm_format_modifier,
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (EGLint)(object.drm_format_modifier >> 32),
|
||||
EGL_NONE
|
||||
};
|
||||
if (m_EGLExtDmaBuf) {
|
||||
const EGLAttrib extra[] = {
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
|
||||
(EGLint)object.drm_format_modifier,
|
||||
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
|
||||
(EGLint)(object.drm_format_modifier >> 32),
|
||||
EGL_NONE,
|
||||
};
|
||||
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
|
||||
|
||||
// Cut off the attribute array before the modifiers if they aren't supported
|
||||
if (!m_EGLExtDmaBuf) {
|
||||
SDL_assert(attribs[12] == EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT);
|
||||
SDL_assert(attribs[14] == EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT);
|
||||
SDL_assert(attribs[16] == EGL_NONE);
|
||||
attribs[12] = EGL_NONE;
|
||||
}
|
||||
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, attribs);
|
||||
if (!images[i]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImage() Failed: %d", eglGetError());
|
||||
goto create_image_fail;
|
||||
|
||||
if (m_eglCreateImage) {
|
||||
images[i] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, attribs);
|
||||
if (!images[i]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImage() Failed: %d", eglGetError());
|
||||
goto create_image_fail;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Cast the EGLAttrib array elements to EGLint for the KHR extension
|
||||
EGLint intAttribs[EGL_ATTRIB_COUNT];
|
||||
for (int i = 0; i < EGL_ATTRIB_COUNT; i++) {
|
||||
intAttribs[i] = (EGLint)attribs[i];
|
||||
}
|
||||
|
||||
images[i] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, intAttribs);
|
||||
if (!images[i]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImageKHR() Failed: %d", eglGetError());
|
||||
goto create_image_fail;
|
||||
}
|
||||
}
|
||||
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
@@ -595,7 +607,12 @@ sync_fail:
|
||||
void
|
||||
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
|
||||
eglDestroyImage(dpy, images[i]);
|
||||
if (m_eglDestroyImage) {
|
||||
m_eglDestroyImage(dpy, images[i]);
|
||||
}
|
||||
else {
|
||||
m_eglDestroyImageKHR(dpy, images[i]);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
|
||||
close(m_PrimeDescriptor.objects[i].fd);
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
virtual int getDecoderColorspace() override;
|
||||
#ifdef HAVE_EGL
|
||||
virtual bool canExportEGL() override;
|
||||
virtual AVPixelFormat getEGLImagePixelFormat() override;
|
||||
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||
@@ -54,7 +55,6 @@ private:
|
||||
|
||||
int m_WindowSystem;
|
||||
AVBufferRef* m_HwContext;
|
||||
int m_DrmFd;
|
||||
bool m_BlacklistedForDirectRendering;
|
||||
|
||||
#ifdef HAVE_LIBVA_X11
|
||||
@@ -69,5 +69,9 @@ private:
|
||||
#ifdef HAVE_EGL
|
||||
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
|
||||
bool m_EGLExtDmaBuf;
|
||||
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
|
||||
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
|
||||
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
|
||||
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -281,8 +281,8 @@ public:
|
||||
// Queue this sample for the next v-sync
|
||||
CMSampleTimingInfo timingInfo = {
|
||||
.duration = kCMTimeInvalid,
|
||||
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time()),
|
||||
.decodeTimeStamp = kCMTimeInvalid,
|
||||
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time())
|
||||
};
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
@@ -365,6 +365,7 @@ public:
|
||||
if ([device.name containsString:@" 5"]) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No HEVC Main10 support on Skylake iGPU");
|
||||
[device release];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -375,10 +376,13 @@ public:
|
||||
[device.name containsString:@" M3"]) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No HEVC Main10 support on AMD GPUs until Polaris");
|
||||
[device release];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[device release];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
|
||||
+176
-124
@@ -51,7 +51,7 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||
|
||||
bool FFmpegVideoDecoder::isAlwaysFullScreen()
|
||||
{
|
||||
return m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
|
||||
return m_FrontendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
|
||||
}
|
||||
|
||||
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||
@@ -117,7 +117,8 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||
}
|
||||
|
||||
FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
||||
: m_VideoDecoderCtx(nullptr),
|
||||
: m_Pkt(av_packet_alloc()),
|
||||
m_VideoDecoderCtx(nullptr),
|
||||
m_DecodeBuffer(1024 * 1024, 0),
|
||||
m_HwDecodeCfg(nullptr),
|
||||
m_BackendRenderer(nullptr),
|
||||
@@ -132,8 +133,6 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
||||
m_NeedsSpsFixup(false),
|
||||
m_TestOnly(testOnly)
|
||||
{
|
||||
av_init_packet(&m_Pkt);
|
||||
|
||||
SDL_zero(m_ActiveWndVideoStats);
|
||||
SDL_zero(m_LastWndVideoStats);
|
||||
SDL_zero(m_GlobalVideoStats);
|
||||
@@ -151,6 +150,8 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
||||
// to preserve the log level across reset() during
|
||||
// test initialization.
|
||||
av_log_set_level(AV_LOG_INFO);
|
||||
|
||||
av_packet_free(&m_Pkt);
|
||||
}
|
||||
|
||||
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
|
||||
@@ -192,17 +193,22 @@ void FFmpegVideoDecoder::reset()
|
||||
}
|
||||
}
|
||||
|
||||
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
||||
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly)
|
||||
{
|
||||
if (eglOnly) {
|
||||
#ifdef HAVE_EGL
|
||||
if (m_BackendRenderer->canExportEGL()) {
|
||||
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
|
||||
if (m_FrontendRenderer->initialize(params)) {
|
||||
return true;
|
||||
if (m_BackendRenderer->canExportEGL()) {
|
||||
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
|
||||
if (m_FrontendRenderer->initialize(params)) {
|
||||
return true;
|
||||
}
|
||||
delete m_FrontendRenderer;
|
||||
m_FrontendRenderer = nullptr;
|
||||
}
|
||||
delete m_FrontendRenderer;
|
||||
}
|
||||
#endif
|
||||
// If we made it here, we failed to create the EGLRenderer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_BackendRenderer->isDirectRenderingSupported()) {
|
||||
// The backend renderer can render to the display
|
||||
@@ -220,13 +226,13 @@ bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame)
|
||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly)
|
||||
{
|
||||
// In test-only mode, we should only see test frames
|
||||
SDL_assert(!m_TestOnly || testFrame);
|
||||
|
||||
// Create the frontend renderer based on the capabilities of the backend renderer
|
||||
if (!createFrontendRenderer(params)) {
|
||||
if (!createFrontendRenderer(params, eglOnly)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -311,16 +317,16 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
||||
if (testFrame) {
|
||||
switch (params->videoFormat) {
|
||||
case VIDEO_FORMAT_H264:
|
||||
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
||||
m_Pkt.size = sizeof(k_H264TestFrame);
|
||||
m_Pkt->data = (uint8_t*)k_H264TestFrame;
|
||||
m_Pkt->size = sizeof(k_H264TestFrame);
|
||||
break;
|
||||
case VIDEO_FORMAT_H265:
|
||||
m_Pkt.data = (uint8_t*)k_HEVCMainTestFrame;
|
||||
m_Pkt.size = sizeof(k_HEVCMainTestFrame);
|
||||
m_Pkt->data = (uint8_t*)k_HEVCMainTestFrame;
|
||||
m_Pkt->size = sizeof(k_HEVCMainTestFrame);
|
||||
break;
|
||||
case VIDEO_FORMAT_H265_MAIN10:
|
||||
m_Pkt.data = (uint8_t*)k_HEVCMain10TestFrame;
|
||||
m_Pkt.size = sizeof(k_HEVCMain10TestFrame);
|
||||
m_Pkt->data = (uint8_t*)k_HEVCMain10TestFrame;
|
||||
m_Pkt->size = sizeof(k_HEVCMain10TestFrame);
|
||||
break;
|
||||
default:
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -341,7 +347,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
||||
for (int retries = 0; retries < 5; retries++) {
|
||||
// Most FFmpeg decoders process input using a "push" model.
|
||||
// We'll see those fail here if the format is not supported.
|
||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||
err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
|
||||
if (err < 0) {
|
||||
av_frame_free(&frame);
|
||||
char errorstring[512];
|
||||
@@ -364,6 +370,14 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
||||
}
|
||||
}
|
||||
|
||||
// Allow the renderer to do any validation it wants on this frame
|
||||
if (!m_FrontendRenderer->testRenderFrame(frame)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Render test failed");
|
||||
av_frame_free(&frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
av_frame_free(&frame);
|
||||
if (err < 0) {
|
||||
char errorstring[512];
|
||||
@@ -522,6 +536,10 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig
|
||||
#ifdef HAVE_LIBVDPAU
|
||||
case AV_HWDEVICE_TYPE_VDPAU:
|
||||
return new VDPAURenderer();
|
||||
#endif
|
||||
#ifdef HAVE_DRM
|
||||
case AV_HWDEVICE_TYPE_DRM:
|
||||
return new DrmRenderer();
|
||||
#endif
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -550,42 +568,135 @@ bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder,
|
||||
const AVCodecHWConfig* hwConfig,
|
||||
std::function<IFFmpegRenderer*()> createRendererFunc)
|
||||
{
|
||||
m_BackendRenderer = createRendererFunc();
|
||||
m_HwDecodeCfg = hwConfig;
|
||||
|
||||
if (m_BackendRenderer != nullptr &&
|
||||
m_BackendRenderer->initialize(params) &&
|
||||
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame())) {
|
||||
if (m_TestOnly) {
|
||||
// This decoder is only for testing capabilities, so don't bother
|
||||
// creating a usable renderer
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_BackendRenderer->needsTestFrame()) {
|
||||
// The test worked, so now let's initialize it for real
|
||||
reset();
|
||||
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
||||
m_BackendRenderer->initialize(params) &&
|
||||
completeInitialization(decoder, params, false)) {
|
||||
// i == 0 - Indirect via EGL frontend with zero-copy DMA-BUF passing
|
||||
// i == 1 - Direct rendering or indirect via SDL read-back
|
||||
#ifdef HAVE_EGL
|
||||
for (int i = 0; i < 2; i++) {
|
||||
#else
|
||||
for (int i = 1; i < 2; i++) {
|
||||
#endif
|
||||
SDL_assert(m_BackendRenderer == nullptr);
|
||||
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
||||
m_BackendRenderer->initialize(params) &&
|
||||
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame(), i == 0 /* EGL */)) {
|
||||
if (m_TestOnly) {
|
||||
// This decoder is only for testing capabilities, so don't bother
|
||||
// creating a usable renderer
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Decoder failed to initialize after successful test");
|
||||
|
||||
if (m_BackendRenderer->needsTestFrame()) {
|
||||
// The test worked, so now let's initialize it for real
|
||||
reset();
|
||||
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
||||
m_BackendRenderer->initialize(params) &&
|
||||
completeInitialization(decoder, params, false, i == 0 /* EGL */)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Decoder failed to initialize after successful test");
|
||||
reset();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No test required. Good to go now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No test required. Good to go now.
|
||||
// Failed to initialize, so keep looking
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
// reset() must be called before we reach this point!
|
||||
SDL_assert(m_BackendRenderer == nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
#define TRY_PREFERRED_PIXEL_FORMAT(RENDERER_TYPE) \
|
||||
{ \
|
||||
RENDERER_TYPE renderer; \
|
||||
if (renderer.getPreferredPixelFormat(params->videoFormat) == decoder->pix_fmts[i]) { \
|
||||
if (tryInitializeRenderer(decoder, params, nullptr, \
|
||||
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
|
||||
"Chose " #RENDERER_TYPE " for codec %s due to preferred pixel format: 0x%x", \
|
||||
decoder->name, decoder->pix_fmts[i]); \
|
||||
return true; \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
#define TRY_SUPPORTED_PIXEL_FORMAT(RENDERER_TYPE) \
|
||||
{ \
|
||||
RENDERER_TYPE renderer; \
|
||||
if (renderer.isPixelFormatSupported(params->videoFormat, decoder->pix_fmts[i])) { \
|
||||
if (tryInitializeRenderer(decoder, params, nullptr, \
|
||||
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
|
||||
"Chose " #RENDERER_TYPE " for codec %s due to compatible pixel format: 0x%x", \
|
||||
decoder->name, decoder->pix_fmts[i]); \
|
||||
return true; \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
bool FFmpegVideoDecoder::tryInitializeRendererForDecoderByName(const char *decoderName,
|
||||
PDECODER_PARAMETERS params)
|
||||
{
|
||||
AVCodec* decoder = avcodec_find_decoder_by_name(decoderName);
|
||||
if (decoder == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This might be a hwaccel decoder, so try any hw configs first
|
||||
for (int i = 0;; i++) {
|
||||
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
|
||||
if (!config) {
|
||||
// No remaing hwaccel options
|
||||
break;
|
||||
}
|
||||
|
||||
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||
if (tryInitializeRenderer(decoder, params, config,
|
||||
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 0); })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Failed to initialize or test frame failed, so keep looking
|
||||
reset();
|
||||
|
||||
if (decoder->pix_fmts == NULL) {
|
||||
// Supported output pixel formats are unknown. We'll just try SDL and hope it can cope.
|
||||
return tryInitializeRenderer(decoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); });
|
||||
}
|
||||
|
||||
// Check if any of our decoders prefer any of the pixel formats first
|
||||
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
|
||||
#ifdef HAVE_DRM
|
||||
TRY_PREFERRED_PIXEL_FORMAT(DrmRenderer);
|
||||
#endif
|
||||
#ifdef HAVE_MMAL
|
||||
TRY_PREFERRED_PIXEL_FORMAT(MmalRenderer);
|
||||
#endif
|
||||
TRY_PREFERRED_PIXEL_FORMAT(SdlRenderer);
|
||||
}
|
||||
|
||||
// Nothing prefers any of them. Let's see if anyone will tolerate one.
|
||||
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
|
||||
#ifdef HAVE_DRM
|
||||
TRY_SUPPORTED_PIXEL_FORMAT(DrmRenderer);
|
||||
#endif
|
||||
#ifdef HAVE_MMAL
|
||||
TRY_SUPPORTED_PIXEL_FORMAT(MmalRenderer);
|
||||
#endif
|
||||
TRY_SUPPORTED_PIXEL_FORMAT(SdlRenderer);
|
||||
}
|
||||
|
||||
// If we made it here, we couldn't find anything
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -595,21 +706,18 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
||||
av_log_set_level(AV_LOG_DEBUG);
|
||||
|
||||
// First try decoders that the user has manually specified via environment variables.
|
||||
// These must output surfaces in one of the formats that the SDL renderer supports,
|
||||
// These must output surfaces in one of the formats that one of our renderers supports,
|
||||
// which is currently:
|
||||
// - AV_PIX_FMT_YUV420P (preferred)
|
||||
// - AV_PIX_FMT_DRM_PRIME
|
||||
// - AV_PIX_FMT_MMAL
|
||||
// - AV_PIX_FMT_YUV420P
|
||||
// - AV_PIX_FMT_NV12
|
||||
// - AV_PIX_FMT_NV21
|
||||
// These formats should cover most/all decoders that output in a standard YUV format.
|
||||
{
|
||||
QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
|
||||
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
|
||||
QByteArray decoderString = h264DecoderHint.toLocal8Bit();
|
||||
AVCodec* customAvcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
|
||||
|
||||
if (customAvcDecoder != nullptr &&
|
||||
tryInitializeRenderer(customAvcDecoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
||||
if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using custom H.264 decoder (H264_DECODER_HINT): %s",
|
||||
decoderString.constData());
|
||||
@@ -626,11 +734,7 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
||||
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
|
||||
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
|
||||
QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
|
||||
AVCodec* customHevcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
|
||||
|
||||
if (customHevcDecoder != nullptr &&
|
||||
tryInitializeRenderer(customHevcDecoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
||||
if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
|
||||
decoderString.constData());
|
||||
@@ -682,74 +786,22 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
||||
}
|
||||
|
||||
// Continue with special non-hwaccel hardware decoders
|
||||
|
||||
#ifdef HAVE_MMAL
|
||||
// MMAL is the decoder for the Raspberry Pi
|
||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
AVCodec* mmalDecoder = avcodec_find_decoder_by_name("h264_mmal");
|
||||
if (mmalDecoder != nullptr &&
|
||||
tryInitializeRenderer(mmalDecoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new MmalRenderer(); })) {
|
||||
return true;
|
||||
QList<const char *> knownAvcCodecs = { "h264_mmal", "h264_rkmpp", "h264_nvmpi", "h264_v4l2m2m" };
|
||||
for (const char* codec : knownAvcCodecs) {
|
||||
if (tryInitializeRendererForDecoderByName(codec, params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_DRM
|
||||
{
|
||||
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
|
||||
AVCodec* rkmppDecoder;
|
||||
|
||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
|
||||
}
|
||||
else {
|
||||
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
|
||||
}
|
||||
|
||||
if (rkmppDecoder != nullptr &&
|
||||
tryInitializeRenderer(rkmppDecoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
|
||||
return true;
|
||||
else {
|
||||
QList<const char *> knownHevcCodecs = { "hevc_rkmpp", "hevc_nvmpi", "hevc_v4l2m2m" };
|
||||
for (const char* codec : knownHevcCodecs) {
|
||||
if (tryInitializeRendererForDecoderByName(codec, params)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
{
|
||||
AVCodec* nvmpiDecoder;
|
||||
|
||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
nvmpiDecoder = avcodec_find_decoder_by_name("h264_nvmpi");
|
||||
}
|
||||
else {
|
||||
nvmpiDecoder = avcodec_find_decoder_by_name("hevc_nvmpi");
|
||||
}
|
||||
|
||||
if (nvmpiDecoder != nullptr &&
|
||||
tryInitializeRenderer(nvmpiDecoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
AVCodec* v4l2Decoder;
|
||||
|
||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
v4l2Decoder = avcodec_find_decoder_by_name("h264_v4l2m2m");
|
||||
}
|
||||
else {
|
||||
v4l2Decoder = avcodec_find_decoder_by_name("hevc_v4l2m2m");
|
||||
}
|
||||
|
||||
if (v4l2Decoder != nullptr &&
|
||||
tryInitializeRenderer(v4l2Decoder, params, nullptr,
|
||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Look for the first matching hwaccel hardware decoder (pass 1)
|
||||
// This picks up "second-tier" hwaccels like CUDA.
|
||||
@@ -881,19 +933,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
||||
m_Pkt.size = offset;
|
||||
m_Pkt->data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
||||
m_Pkt->size = offset;
|
||||
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
m_Pkt.flags = AV_PKT_FLAG_KEY;
|
||||
m_Pkt->flags = AV_PKT_FLAG_KEY;
|
||||
}
|
||||
else {
|
||||
m_Pkt.flags = 0;
|
||||
m_Pkt->flags = 0;
|
||||
}
|
||||
|
||||
m_ActiveWndVideoStats.totalReassemblyTime += du->enqueueTimeMs - du->receiveTimeMs;
|
||||
|
||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||
err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
|
||||
if (err < 0) {
|
||||
char errorstring[512];
|
||||
av_strerror(err, errorstring, sizeof(errorstring));
|
||||
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
virtual IFFmpegRenderer* getBackendRenderer();
|
||||
|
||||
private:
|
||||
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame);
|
||||
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly);
|
||||
|
||||
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
|
||||
|
||||
@@ -34,7 +34,10 @@ private:
|
||||
|
||||
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
|
||||
|
||||
bool createFrontendRenderer(PDECODER_PARAMETERS params);
|
||||
bool createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly);
|
||||
|
||||
bool tryInitializeRendererForDecoderByName(const char* decoderName,
|
||||
PDECODER_PARAMETERS params);
|
||||
|
||||
bool tryInitializeRenderer(AVCodec* decoder,
|
||||
PDECODER_PARAMETERS params,
|
||||
@@ -51,7 +54,7 @@ private:
|
||||
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||
const enum AVPixelFormat* pixFmts);
|
||||
|
||||
AVPacket m_Pkt;
|
||||
AVPacket* m_Pkt;
|
||||
AVCodecContext* m_VideoDecoderCtx;
|
||||
QByteArray m_DecodeBuffer;
|
||||
const AVCodecHWConfig* m_HwDecodeCfg;
|
||||
|
||||
@@ -7,4 +7,5 @@ namespace WMUtils {
|
||||
bool isRunningX11();
|
||||
bool isRunningWayland();
|
||||
bool isRunningWindowManager();
|
||||
bool isRunningDesktopEnvironment();
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.0.0
|
||||
3.1.2
|
||||
+58
-8
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef HAS_X11
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
@@ -10,14 +12,30 @@
|
||||
#include <wayland-client.h>
|
||||
#endif
|
||||
|
||||
#define VALUE_SET 0x01
|
||||
#define VALUE_TRUE 0x02
|
||||
|
||||
bool WMUtils::isRunningX11()
|
||||
{
|
||||
#ifdef HAS_X11
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (display != nullptr) {
|
||||
XCloseDisplay(display);
|
||||
return true;
|
||||
static SDL_atomic_t isRunningOnX11;
|
||||
|
||||
// If the value is not set yet, populate it now.
|
||||
int val = SDL_AtomicGet(&isRunningOnX11);
|
||||
if (!(val & VALUE_SET)) {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (display != nullptr) {
|
||||
XCloseDisplay(display);
|
||||
}
|
||||
|
||||
// Populate the value to return and have for next time.
|
||||
// This can race with another thread populating the same data,
|
||||
// but that's no big deal.
|
||||
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
|
||||
SDL_AtomicSet(&isRunningOnX11, val);
|
||||
}
|
||||
|
||||
return !!(val & VALUE_TRUE);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
@@ -26,11 +44,24 @@ bool WMUtils::isRunningX11()
|
||||
bool WMUtils::isRunningWayland()
|
||||
{
|
||||
#ifdef HAS_WAYLAND
|
||||
struct wl_display* display = wl_display_connect(nullptr);
|
||||
if (display != nullptr) {
|
||||
wl_display_disconnect(display);
|
||||
return true;
|
||||
static SDL_atomic_t isRunningOnWayland;
|
||||
|
||||
// If the value is not set yet, populate it now.
|
||||
int val = SDL_AtomicGet(&isRunningOnWayland);
|
||||
if (!(val & VALUE_SET)) {
|
||||
struct wl_display* display = wl_display_connect(nullptr);
|
||||
if (display != nullptr) {
|
||||
wl_display_disconnect(display);
|
||||
}
|
||||
|
||||
// Populate the value to return and have for next time.
|
||||
// This can race with another thread populating the same data,
|
||||
// but that's no big deal.
|
||||
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
|
||||
SDL_AtomicSet(&isRunningOnWayland, val);
|
||||
}
|
||||
|
||||
return !!(val & VALUE_TRUE);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
@@ -46,3 +77,22 @@ bool WMUtils::isRunningWindowManager()
|
||||
return WMUtils::isRunningWayland() || WMUtils::isRunningX11();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WMUtils::isRunningDesktopEnvironment()
|
||||
{
|
||||
if (qEnvironmentVariableIsSet("HAS_DESKTOP_ENVIRONMENT")) {
|
||||
return qEnvironmentVariableIntValue("HAS_DESKTOP_ENVIRONMENT");
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN)
|
||||
// Windows and macOS are always running a desktop environment
|
||||
return true;
|
||||
#elif defined(EMBEDDED_BUILD)
|
||||
// Embedded systems don't run desktop environments
|
||||
return false;
|
||||
#else
|
||||
// On non-embedded systems, assume we have a desktop environment
|
||||
// if we have a WM running.
|
||||
return isRunningWindowManager();
|
||||
#endif
|
||||
}
|
||||
|
||||
+7
-7
@@ -17,22 +17,22 @@ environment:
|
||||
install:
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || brew install qt'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || brew link --force qt'
|
||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
|
||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update || true'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.14 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_REV=dfa64eadd38a6a8125290dcf121f1bc285e98dad && git clone https://github.com/libsdl-org/SDL.git SDL2 && cd SDL2 && git checkout $SDL2_REV && ./configure --enable-video-kmsdrm && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.10.0 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.11.0 && git clone https://github.com/intel/libva.git && cd libva && git checkout $LIBVA_VER && ./autogen.sh && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.12 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
|
||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
|
||||
|
||||
before_build:
|
||||
- 'git submodule update --init --recursive'
|
||||
- 'git -c submodule.libs.update=none submodule update --init --recursive'
|
||||
- cmd: 'git submodule update --init --recursive'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || git submodule update --init --recursive'
|
||||
- cmd: 'set OLDPATH=%PATH%'
|
||||
- cmd: 'set /p VERSION=<app\version.txt'
|
||||
- sh: 'export VERSION=`cat app/version.txt`'
|
||||
@@ -45,7 +45,7 @@ build_script:
|
||||
- cmd: 'scripts\generate-bundle.bat Release'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt514/bin/qt514-env.sh'
|
||||
- sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || scripts/generate-dmg.sh Release'
|
||||
- sh: '[ "$BUILD_TARGET" != macos ] || PATH=$PATH:$HOME/Qt/5.15/clang_64/bin scripts/generate-dmg.sh Release'
|
||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
|
||||
|
||||
after_build:
|
||||
|
||||
Submodule
+1
Submodule libs added at c5149747ff
@@ -1 +0,0 @@
|
||||
Versions/Current/Headers
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Resources
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/SDL2
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Resources
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
|
||||
HIDAPI Authors:
|
||||
|
||||
Alan Ott <alan@signal11.us>:
|
||||
Original Author and Maintainer
|
||||
Linux, Windows, and Mac implementations
|
||||
|
||||
Ludovic Rousseau <rousseau@debian.org>:
|
||||
Formatting for Doxygen documentation
|
||||
Bug fixes
|
||||
Correctness fixes
|
||||
|
||||
|
||||
For a comprehensive list of contributions, see the commit list at github:
|
||||
https://github.com/libusb/hidapi/commits/master
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildMachineOSBuild</key>
|
||||
<string>19H114</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>hidapi</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.libsdl.hidapi</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>hidapi</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
<string>12C33</string>
|
||||
<key>DTPlatformName</key>
|
||||
<string>macosx</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>11.1</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>20C63</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>macosx11.1</string>
|
||||
<key>DTXcode</key>
|
||||
<string>1230</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>12C33</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.6</string>
|
||||
</dict>
|
||||
</plist>
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
Copyright (c) 2010, Alan Ott, Signal 11 Software
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Signal 11 Software nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
-674
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
HIDAPI - Multi-Platform library for
|
||||
communication with HID devices.
|
||||
|
||||
Copyright 2009, Alan Ott, Signal 11 Software.
|
||||
All Rights Reserved.
|
||||
|
||||
This software may be used by anyone for any reason so
|
||||
long as the copyright notice in the source files
|
||||
remains intact.
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
HIDAPI can be used under one of three licenses.
|
||||
|
||||
1. The GNU General Public License, version 3.0, in LICENSE-gpl3.txt
|
||||
2. A BSD-Style License, in LICENSE-bsd.txt.
|
||||
3. The more liberal original HIDAPI license. LICENSE-orig.txt
|
||||
|
||||
The license chosen is at the discretion of the user of HIDAPI. For example:
|
||||
1. An author of GPL software would likely use HIDAPI under the terms of the
|
||||
GPL.
|
||||
|
||||
2. An author of commercial closed-source software would likely use HIDAPI
|
||||
under the terms of the BSD-style license or the original HIDAPI license.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user