mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-18 06:24:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
394f28339e | ||
|
|
411998d4e3 | ||
|
|
371906445e | ||
|
|
34d91e58c0 | ||
|
|
f3f25bec7a | ||
|
|
08d74bea75 | ||
|
|
63f917b317 | ||
|
|
448633ec3c | ||
|
|
09ce3c9ce4 | ||
|
|
8fcc98102b | ||
|
|
e14075b464 | ||
|
|
ca89d14ec8 | ||
|
|
3f51998065 | ||
|
|
6e5b6a1a5c | ||
|
|
660f6c5bbf | ||
|
|
5034bd72ed | ||
|
|
5b7c144796 | ||
|
|
e45b6b9b85 | ||
|
|
245f242fb8 | ||
|
|
a6fccf93d1 | ||
|
|
3dcab90752 | ||
|
|
30a5f405f5 | ||
|
|
bad1c9628e | ||
|
|
1ab7bca43f | ||
|
|
7d0a53773c | ||
|
|
789236ee8f | ||
|
|
21cba0b080 | ||
|
|
31eb0891e5 | ||
|
|
a13f8e7937 | ||
|
|
5bfe56472f | ||
|
|
16a9ed682e | ||
|
|
59cae610d2 | ||
|
|
29ac7893d7 | ||
|
|
c8aeca8ace | ||
|
|
3da505c4c6 | ||
|
|
06bc05b708 | ||
|
|
a3dc9593c2 | ||
|
|
18a271a0ec | ||
|
|
cebd3bf751 | ||
|
|
cb04925c6e | ||
|
|
b7a340bd00 | ||
|
|
2a0f837120 | ||
|
|
d00bdff495 | ||
|
|
7e9c977b3d | ||
|
|
1629151528 | ||
|
|
24295197bd | ||
|
|
807dd84daa | ||
|
|
17848c097c | ||
|
|
563b760d31 | ||
|
|
8465a27a97 | ||
|
|
255cbd1e7e | ||
|
|
7717749b72 | ||
|
|
9e4337babf | ||
|
|
6771877a84 | ||
|
|
a860bd8dd5 | ||
|
|
704ce4ff0c | ||
|
|
21edcc8113 | ||
|
|
125738f13d | ||
|
|
02c781b8f4 | ||
|
|
453fe6aea0 | ||
|
|
e0fb7dfd14 | ||
|
|
c4012225c2 | ||
|
|
1750243537 | ||
|
|
84fc5f3008 | ||
|
|
8a224fbae4 | ||
|
|
e6c4332445 | ||
|
|
0928d1fab2 | ||
|
|
40dd588bef | ||
|
|
472e8ee92e | ||
|
|
c55af7fff3 | ||
|
|
7a39daf179 | ||
|
|
8d087ba5b1 | ||
|
|
c7a30d8728 | ||
|
|
1f678cafba | ||
|
|
579e0bc464 | ||
|
|
7f36dc5aad | ||
|
|
a7c2026128 | ||
|
|
7b0d67ebbe | ||
|
|
63f0910eac | ||
|
|
b16cf9fcc5 | ||
|
|
5501d7bc6b | ||
|
|
413ef42a34 | ||
|
|
a1694ab951 | ||
|
|
fbbbd5e0b8 | ||
|
|
f8c7e2ee41 | ||
|
|
2a4af0bc2c | ||
|
|
d24db13e9d | ||
|
|
bc68aad67b | ||
|
|
9e8a4cab7b | ||
|
|
3679964cf8 | ||
|
|
225ec1a87f | ||
|
|
071c76c06a | ||
|
|
d77a255721 | ||
|
|
26ee1aee25 | ||
|
|
b047395938 | ||
|
|
483a601867 | ||
|
|
c4bc2f6adf | ||
|
|
439e8fe33f | ||
|
|
99885f5b4b | ||
|
|
78b522ec1d | ||
|
|
dbfa5a9358 | ||
|
|
3910b3862a | ||
|
|
3b8e3b7d5c | ||
|
|
6129000dde | ||
|
|
65b274d695 | ||
|
|
a13dd0076b | ||
|
|
cc026c995c | ||
|
|
81cfacaa3d | ||
|
|
fabebc05d2 | ||
|
|
07a97bab03 | ||
|
|
69ac810c82 | ||
|
|
3f4609dc6e | ||
|
|
1ddb529d26 | ||
|
|
ab6b88073c | ||
|
|
ad3ed97472 | ||
|
|
07e902fe95 | ||
|
|
eb781ce689 | ||
|
|
555a65c8ea | ||
|
|
58429f4172 | ||
|
|
e2692a0abd | ||
|
|
d218c43edf | ||
|
|
fae9813127 | ||
|
|
adc56d4ee5 | ||
|
|
f982e33fab | ||
|
|
9ab4479fcf | ||
|
|
3e330b7929 | ||
|
|
ecddada150 | ||
|
|
0c828cbb37 | ||
|
|
84a32f8c16 | ||
|
|
4ee36fd405 | ||
|
|
0802609fd8 | ||
|
|
de88176995 | ||
|
|
aca82f400a | ||
|
|
54bb6fe527 | ||
|
|
c1f663cec1 | ||
|
|
aaf59c2d01 | ||
|
|
692942e5be | ||
|
|
f4acd629a1 | ||
|
|
0ff8a44ec3 | ||
|
|
c6d8f6e618 | ||
|
|
1baa23cc43 | ||
|
|
3be696e6bb | ||
|
|
3cfcb01603 | ||
|
|
d39a29759c | ||
|
|
bf4332b9e7 | ||
|
|
feaae7b684 | ||
|
|
be7852dfc0 | ||
|
|
b0804ce048 | ||
|
|
aa7d5fa924 | ||
|
|
b4cab12e9d | ||
|
|
133dda2de0 | ||
|
|
ef5605ae39 | ||
|
|
9b29b2702e | ||
|
|
abafa270e5 | ||
|
|
15852ccf81 | ||
|
|
a427ecd029 | ||
|
|
77228dcb2c | ||
|
|
3948cfe495 | ||
|
|
36218b54f8 | ||
|
|
be2f4433db | ||
|
|
dd5d36cf07 | ||
|
|
685136d98f | ||
|
|
852937421e | ||
|
|
a5389b7be5 | ||
|
|
1b96b18bc0 | ||
|
|
e9edad6f8c | ||
|
|
f8a6012c3a | ||
|
|
e103f6c9b6 | ||
|
|
9daf7b5a36 | ||
|
|
d6142e1af0 | ||
|
|
b0491210ff | ||
|
|
91d8550290 | ||
|
|
dfaaa7d72a | ||
|
|
5174a4f58a | ||
|
|
b9c73df488 | ||
|
|
f99f1f6441 | ||
|
|
1ec1f5d3be | ||
|
|
713db06a05 | ||
|
|
a0adcbd90b | ||
|
|
b94776421c | ||
|
|
79c1043063 | ||
|
|
cfabae17c0 | ||
|
|
71a1d25e90 | ||
|
|
c2d7b835fb | ||
|
|
f72888ce6f | ||
|
|
30f474d253 | ||
|
|
216f274a92 | ||
|
|
5ea79efe4b | ||
|
|
197c1ba21b | ||
|
|
95c4a24d99 | ||
|
|
f5f23a7c22 | ||
|
|
c469576460 | ||
|
|
0001bb0e4c | ||
|
|
56f184393f | ||
|
|
85777e85fa | ||
|
|
c750ec3cc7 | ||
|
|
c75b781d3f | ||
|
|
8aa42ff1d9 | ||
|
|
63a5bc5e12 | ||
|
|
e0b7b6dc3d | ||
|
|
3d7ecf98c2 | ||
|
|
ad5581c620 | ||
|
|
69af991772 | ||
|
|
2bb2745f91 | ||
|
|
5f682bb45f | ||
|
|
48bcb6b9a6 | ||
|
|
092c0a7392 | ||
|
|
4e8618c512 | ||
|
|
c51a69c0dc | ||
|
|
2480849f39 | ||
|
|
6bfe40607d | ||
|
|
a1c350a537 | ||
|
|
f732b1978e | ||
|
|
d3a542bac4 | ||
|
|
b12724db02 | ||
|
|
f8f5e8e869 | ||
|
|
0733f06103 | ||
|
|
6fdaa6ee8a | ||
|
|
f10b547a49 | ||
|
|
38396b2691 | ||
|
|
220f50ebe4 | ||
|
|
c989133d27 | ||
|
|
6c339551eb | ||
|
|
ef23a80b16 | ||
|
|
a521093731 | ||
|
|
747d3eedf1 | ||
|
|
fd563726f0 | ||
|
|
62136a1a96 | ||
|
|
cf16c31d38 | ||
|
|
0f5d4aa72f | ||
|
|
d8dbe77427 | ||
|
|
0e5670d50d | ||
|
|
22924a1e92 | ||
|
|
e3a7b54f90 | ||
|
|
81d5e7f014 | ||
|
|
9790c218dc | ||
|
|
6d3d51553b | ||
|
|
474591c6a5 | ||
|
|
5de0dc2713 | ||
|
|
57a34a7abb | ||
|
|
54517bda1b | ||
|
|
4291e15b60 | ||
|
|
940ae6406b | ||
|
|
6a0cf4bfd3 | ||
|
|
fee6f75223 | ||
|
|
8c93b2d54c | ||
|
|
dbbb1bd8a1 | ||
|
|
5c838b2fc5 | ||
|
|
3a094d4ba8 | ||
|
|
5d11c1ea6a | ||
|
|
a7e2e15eac | ||
|
|
585a0ca1f4 | ||
|
|
09ed82db65 | ||
|
|
a56cb0e88a | ||
|
|
9acf5ff0ef | ||
|
|
64a7790aed | ||
|
|
9a5134ffcf | ||
|
|
1d2c4d0843 | ||
|
|
efad34be62 | ||
|
|
52761ade87 | ||
|
|
e6b816e776 | ||
|
|
6d3bcba5d0 | ||
|
|
2e7ffa606b | ||
|
|
055cae90b8 | ||
|
|
23acf92803 | ||
|
|
91ee7d76eb | ||
|
|
570888e2b8 | ||
|
|
282f27f0c8 | ||
|
|
98376ade45 | ||
|
|
9682968b55 | ||
|
|
877cb5f1b0 | ||
|
|
6972573a6e | ||
|
|
742f1b1283 | ||
|
|
0b127a2f3b | ||
|
|
a6057835a8 | ||
|
|
c32794477e | ||
|
|
999ac60bda | ||
|
|
bca4b832a8 | ||
|
|
2fc1dda232 | ||
|
|
99495cff90 | ||
|
|
795962a4ba | ||
|
|
7bde9f447a | ||
|
|
b45c3e3c7f | ||
|
|
ce2a01fc59 | ||
|
|
edeea65c3e | ||
|
|
6e8ee94b91 | ||
|
|
97d8274911 | ||
|
|
73d84dc13f | ||
|
|
f32501d498 | ||
|
|
9c289c3fe9 | ||
|
|
2a610584ee | ||
|
|
8e06421567 | ||
|
|
57dc3725f5 | ||
|
|
d6ef8945f5 | ||
|
|
e078a8b7b6 | ||
|
|
a6395b9025 | ||
|
|
6c0983d77c | ||
|
|
4ae39f2bd5 | ||
|
|
71fc701809 | ||
|
|
d418921e4b | ||
|
|
4b83b022d3 | ||
|
|
8b13e5b47e | ||
|
|
0bfd565bda | ||
|
|
2224a558ee | ||
|
|
93c02fb43a | ||
|
|
93ee51cabb | ||
|
|
cd6ba554a4 | ||
|
|
89aa2be358 | ||
|
|
da261246b9 | ||
|
|
4d3e09f4a0 | ||
|
|
9add6b3696 | ||
|
|
ef037e18d0 | ||
|
|
36db791516 | ||
|
|
25d2ea5e94 | ||
|
|
34f2d18f27 | ||
|
|
4935c846aa | ||
|
|
b1f980c216 | ||
|
|
738f64c903 | ||
|
|
7235663447 | ||
|
|
9d78c416f7 | ||
|
|
9a64c026ea | ||
|
|
be2af1d17a | ||
|
|
b21131a466 | ||
|
|
41c5126472 | ||
|
|
b96cb1abaf | ||
|
|
20143606e2 | ||
|
|
1d79bcc657 | ||
|
|
e8b4494fc3 | ||
|
|
2518208c5c | ||
|
|
918fea7d4b | ||
|
|
4dc07bf63f | ||
|
|
04a0aca221 | ||
|
|
1942e72474 | ||
|
|
f256407789 | ||
|
|
67612f607e | ||
|
|
7274c42126 | ||
|
|
e288402e88 | ||
|
|
76bd4a1c7b | ||
|
|
13dbad6f35 | ||
|
|
cf089fdfe9 | ||
|
|
9755e31fa2 | ||
|
|
3fcf31d009 | ||
|
|
1cbdd0e675 | ||
|
|
e0fd84d6f5 | ||
|
|
443720c3de | ||
|
|
254526a213 | ||
|
|
ad0afb5fc9 | ||
|
|
e64b15d2a7 | ||
|
|
cdc3352318 | ||
|
|
2416618b34 | ||
|
|
c951d77a52 | ||
|
|
759ddc0b4b | ||
|
|
a0709bec51 | ||
|
|
f5bc4e7a24 | ||
|
|
b4665b6e5a | ||
|
|
b85d5b8822 | ||
|
|
12ad75a6b0 | ||
|
|
af92fe6ad1 | ||
|
|
fcf7ed4faa | ||
|
|
a58649fd10 | ||
|
|
ee5918cf4e | ||
|
|
d7de3301ed | ||
|
|
bd1988c775 | ||
|
|
474010b31d | ||
|
|
dd941154d6 | ||
|
|
cbb53745f6 | ||
|
|
3dff62e0cf |
@@ -12,8 +12,11 @@ contains(QT_ARCH, i386) {
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += -lNktHookLib
|
||||
LIBS += -ldetours
|
||||
DEFINES += ANTIHOOKING_LIBRARY
|
||||
SOURCES += antihookingprotection.cpp
|
||||
HEADERS += antihookingprotection.h
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "antihookingprotection.h"
|
||||
|
||||
#include <NktHookLib.h>
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <detours.h>
|
||||
|
||||
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
||||
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
||||
@@ -12,25 +15,35 @@ class AntiHookingProtection
|
||||
public:
|
||||
static void enable()
|
||||
{
|
||||
#ifdef QT_DEBUG
|
||||
s_HookManager.SetEnableDebugOutput(true);
|
||||
#endif
|
||||
DetourTransactionBegin();
|
||||
DetourUpdateThread(GetCurrentThread());
|
||||
|
||||
HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
|
||||
SIZE_T hookId;
|
||||
s_RealLoadLibraryA = LoadLibraryA;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
|
||||
s_RealLoadLibraryW = LoadLibraryW;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||
|
||||
s_RealLoadLibraryExA = LoadLibraryExA;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||
|
||||
s_RealLoadLibraryExW = LoadLibraryExW;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||
|
||||
DetourTransactionCommit();
|
||||
}
|
||||
|
||||
static void disable()
|
||||
{
|
||||
DetourTransactionBegin();
|
||||
DetourUpdateThread(GetCurrentThread());
|
||||
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||
|
||||
DetourTransactionCommit();
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -124,7 +137,6 @@ private:
|
||||
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static CNktHookLib s_HookManager;
|
||||
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||
@@ -209,7 +221,6 @@ private:
|
||||
};
|
||||
};
|
||||
|
||||
CNktHookLib AntiHookingProtection::s_HookManager;
|
||||
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||
@@ -218,14 +229,26 @@ LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
||||
AH_EXPORT void AntiHookingDummyImport() {}
|
||||
|
||||
extern "C"
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
||||
{
|
||||
if (DetourIsHelperProcess()) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
switch (fdwReason)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
DetourRestoreAfterWith();
|
||||
AntiHookingProtection::enable();
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
// Ignore DLL_PROCESS_DETACH on process exit. No need to waste time
|
||||
// unhooking everything if the whole process is being destroyed.
|
||||
if (lpvReserved == NULL) {
|
||||
AntiHookingProtection::disable();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Moonlight PC
|
||||
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream, as used by the NVIDIA Shield.
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
@@ -14,6 +14,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- HDR streaming support
|
||||
- 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
|
||||
@@ -55,7 +56,7 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
||||
* Qt 5.9 SDK or later
|
||||
* 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`
|
||||
* 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 qtwayland5 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 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/
|
||||
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: 2924d8a562...ff26eb04d0
+30
-11
@@ -54,6 +54,11 @@ macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
|
||||
# QMake doesn't handle framework-style includes correctly on its own
|
||||
QMAKE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_CXXFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
@@ -108,14 +113,14 @@ unix:!macx {
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
}
|
||||
macx {
|
||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lssl -lcrypto -lavcodec.59 -lavutil.57 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
||||
|
||||
# For libsoundio
|
||||
@@ -127,6 +132,7 @@ macx {
|
||||
SOURCES += \
|
||||
backend/nvaddress.cpp \
|
||||
backend/nvapp.cpp \
|
||||
cli/pair.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
@@ -137,6 +143,7 @@ SOURCES += \
|
||||
backend/boxartmanager.cpp \
|
||||
backend/richpresencemanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/listapps.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
@@ -165,6 +172,7 @@ SOURCES += \
|
||||
HEADERS += \
|
||||
backend/nvaddress.h \
|
||||
backend/nvapp.h \
|
||||
cli/pair.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
@@ -177,6 +185,7 @@ HEADERS += \
|
||||
backend/boxartmanager.h \
|
||||
backend/richpresencemanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/listapps.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
@@ -262,7 +271,7 @@ libdrm {
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
@@ -296,15 +305,20 @@ config_SL {
|
||||
streaming/video/slvid.h \
|
||||
streaming/audio/renderers/slaud.h
|
||||
}
|
||||
win32 {
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxutil.h
|
||||
}
|
||||
win32:!winrt {
|
||||
message(DXVA2 renderer selected)
|
||||
message(DXVA2 and D3D11VA renderers selected)
|
||||
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
||||
streaming/video/ffmpeg-renderers/d3d11va.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.h \
|
||||
streaming/video/ffmpeg-renderers/d3d11va.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
||||
}
|
||||
macx {
|
||||
@@ -355,7 +369,14 @@ TRANSLATIONS += \
|
||||
languages/qml_sv.ts \
|
||||
languages/qml_tr.ts \
|
||||
languages/qml_uk.ts \
|
||||
languages/qml_zh_Hant.ts
|
||||
languages/qml_zh_Hant.ts \
|
||||
languages/qml_el.ts \
|
||||
languages/qml_hi.ts \
|
||||
languages/qml_it.ts \
|
||||
languages/qml_pt.ts \
|
||||
languages/qml_pt_BR.ts \
|
||||
languages/qml_pl.ts \
|
||||
languages/qml_cs.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
@@ -394,13 +415,11 @@ INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
!winrt {
|
||||
contains(QT_ARCH, i386)|contains(QT_ARCH, x86_64) {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
|
||||
@@ -201,7 +201,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
qWarning() << "Update checking failed with error:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
+130
-26
@@ -10,6 +10,8 @@
|
||||
#include <QThreadPool>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <random>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
|
||||
class PcMonitorThread : public QThread
|
||||
@@ -42,7 +44,7 @@ private:
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||
qInfo() << "Found unexpected PC" << newState.name << "looking for" << m_Computer->name;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -145,7 +147,8 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr),
|
||||
m_CompatFetcher(nullptr)
|
||||
m_CompatFetcher(nullptr),
|
||||
m_NeedsDelayedFlush(false)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
@@ -161,6 +164,10 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
// Fetch latest compatibility data asynchronously
|
||||
m_CompatFetcher.start();
|
||||
|
||||
// Start the delayed flush thread to handle saveHosts() calls
|
||||
m_DelayedFlushThread = new DelayedFlushThread(this);
|
||||
m_DelayedFlushThread->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
|
||||
@@ -171,6 +178,21 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
{
|
||||
// Stop the delayed flush thread before acquiring the lock in write mode
|
||||
// to avoid deadlocking with a flush that needs the lock in read mode.
|
||||
{
|
||||
// Wake the delayed flush thread
|
||||
m_DelayedFlushThread->requestInterruption();
|
||||
m_DelayedFlushCondition.wakeOne();
|
||||
|
||||
// Wait for it to terminate (and finish any pending flush)
|
||||
m_DelayedFlushThread->wait();
|
||||
delete m_DelayedFlushThread;
|
||||
|
||||
// Delayed flushes should have completed by now
|
||||
Q_ASSERT(!m_NeedsDelayedFlush);
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
// Delete machines that haven't been resolved yet
|
||||
@@ -200,19 +222,53 @@ ComputerManager::~ComputerManager()
|
||||
}
|
||||
}
|
||||
|
||||
void DelayedFlushThread::run() {
|
||||
for (;;) {
|
||||
// Wait for a delayed flush request or an interruption
|
||||
{
|
||||
QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
|
||||
m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
|
||||
}
|
||||
|
||||
// Bail without flushing if we woke up for an interruption alone.
|
||||
// If we have both an interruption and a flush request, do the flush.
|
||||
if (!m_ComputerManager->m_NeedsDelayedFlush) {
|
||||
Q_ASSERT(QThread::currentThread()->isInterruptionRequested());
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
||||
m_ComputerManager->m_NeedsDelayedFlush = false;
|
||||
}
|
||||
|
||||
// Perform the flush
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
QSettings settings;
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::saveHosts()
|
||||
{
|
||||
QSettings settings;
|
||||
QReadLocker lock(&m_Lock);
|
||||
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning());
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms)
|
||||
// to persist our host list to disk (especially when a host has a bunch of apps).
|
||||
QMutexLocker locker(&m_DelayedFlushMutex);
|
||||
m_NeedsDelayedFlush = true;
|
||||
m_DelayedFlushCondition.wakeOne();
|
||||
}
|
||||
|
||||
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
||||
@@ -440,7 +496,7 @@ void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||
|
||||
void ComputerManager::handleAboutToQuit()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
// Interrupt polling threads immediately, so they
|
||||
// avoid making additional requests while quitting
|
||||
@@ -455,7 +511,8 @@ class PendingPairingTask : public QObject, public QRunnable
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
@@ -475,25 +532,34 @@ private:
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||
emit pairingCompleted(m_Computer, tr("The PIN from the PC didn't match. Please try again."));
|
||||
break;
|
||||
case NvPairingManager::PairState::FAILED:
|
||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
emit pairingCompleted(m_Computer, tr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."));
|
||||
}
|
||||
else {
|
||||
emit pairingCompleted(m_Computer, tr("Pairing failed. Please try again."));
|
||||
}
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
emit pairingCompleted(m_Computer, tr("Another pairing attempt is already in progress."));
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
// Persist the newly pinned server certificate for this host
|
||||
m_ComputerManager->saveHosts();
|
||||
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
||||
emit pairingCompleted(m_Computer, tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
@@ -611,12 +677,15 @@ public:
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||
m_Mdns(mdns)
|
||||
m_Mdns(mdns),
|
||||
m_AboutToQuit(false)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||
this, &PendingAddTask::handleAboutToQuit);
|
||||
}
|
||||
|
||||
signals:
|
||||
@@ -625,9 +694,20 @@ signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void handleAboutToQuit()
|
||||
{
|
||||
m_AboutToQuit = true;
|
||||
}
|
||||
|
||||
QString fetchServerInfo(NvHTTP& http)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
// Do nothing if we're quitting
|
||||
if (m_AboutToQuit) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
try {
|
||||
// There's a race condition between GameStream servers reporting presence over
|
||||
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
||||
@@ -749,16 +829,31 @@ private:
|
||||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
// Check if this PC already exists using opportunistic read lock
|
||||
m_ComputerManager->m_Lock.lockForRead();
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
|
||||
// If it doesn't already exist, convert to a write lock in preparation for updating.
|
||||
//
|
||||
// NB: ComputerManager's lock protects the host map itself, not the elements inside.
|
||||
// Those are protected by their individual locks. Since we only mutate the map itself
|
||||
// when the PC doesn't exist, we need the lock in write-mode for that case only.
|
||||
if (existingComputer == nullptr) {
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
m_ComputerManager->m_Lock.lockForWrite();
|
||||
|
||||
// Since we had to unlock to lock for write, someone could have raced and added
|
||||
// this PC before us. We have to check again whether it already exists.
|
||||
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
}
|
||||
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
@@ -779,17 +874,15 @@ private:
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
|
||||
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||
// go ahead and do the STUN request now to populate an external address.
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
lock.relock();
|
||||
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
@@ -811,6 +904,7 @@ private:
|
||||
NvAddress m_Address;
|
||||
NvAddress m_MdnsIpv6Address;
|
||||
bool m_Mdns;
|
||||
bool m_AboutToQuit;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
|
||||
@@ -821,4 +915,14 @@ void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerManager::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
}
|
||||
|
||||
#include "computermanager.moc"
|
||||
|
||||
@@ -15,6 +15,27 @@
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
class ComputerManager;
|
||||
|
||||
class DelayedFlushThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DelayedFlushThread(ComputerManager* cm)
|
||||
: m_ComputerManager(cm)
|
||||
{
|
||||
setObjectName("CM Delayed Flush Thread");
|
||||
}
|
||||
|
||||
void run();
|
||||
|
||||
private:
|
||||
ComputerManager* m_ComputerManager;
|
||||
};
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
@@ -166,6 +187,8 @@ class ComputerManager : public QObject
|
||||
|
||||
friend class DeferredHostDeletionTask;
|
||||
friend class PendingAddTask;
|
||||
friend class PendingPairingTask;
|
||||
friend class DelayedFlushThread;
|
||||
|
||||
public:
|
||||
explicit ComputerManager(QObject *parent = nullptr);
|
||||
@@ -180,6 +203,8 @@ public:
|
||||
|
||||
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
|
||||
|
||||
QString generatePinString();
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
void quitRunningApp(NvComputer* computer);
|
||||
@@ -225,4 +250,8 @@ private:
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
DelayedFlushThread* m_DelayedFlushThread;
|
||||
QMutex m_DelayedFlushMutex;
|
||||
QWaitCondition m_DelayedFlushCondition;
|
||||
bool m_NeedsDelayedFlush;
|
||||
};
|
||||
|
||||
+66
-33
@@ -21,6 +21,7 @@
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
#define SER_NVIDIASOFTWARE "nvidiasw"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
@@ -37,6 +38,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
|
||||
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
this->isNvidiaServerSoftware = settings.value(SER_NVIDIASOFTWARE).toBool();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
@@ -65,6 +67,8 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
|
||||
void NvComputer::setRemoteAddress(QHostAddress address)
|
||||
{
|
||||
QWriteLocker lock(&this->lock);
|
||||
|
||||
Q_ASSERT(this->externalPort != 0);
|
||||
|
||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||
@@ -87,6 +91,7 @@ void NvComputer::serialize(QSettings& settings) const
|
||||
settings.setValue(SER_MANUALADDR, manualAddress.address());
|
||||
settings.setValue(SER_MANUALPORT, manualAddress.port());
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
@@ -175,6 +180,11 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->remoteAddress = NvAddress();
|
||||
}
|
||||
|
||||
// Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir'
|
||||
// codename in the state field and no version of Sunshine does. We can use this to bypass
|
||||
// some assumptions about Nvidia hardware that don't apply to Sunshine hosts.
|
||||
this->isNvidiaServerSoftware = NvHTTP::getXmlString(serverInfo, "state").contains("MJOLNIR");
|
||||
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
@@ -187,16 +197,29 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
bool NvComputer::wake() const
|
||||
{
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
}
|
||||
QByteArray wolPayload;
|
||||
|
||||
if (macAddress.isEmpty()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macAddress.isEmpty()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the WoL payload
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
@@ -205,14 +228,6 @@ bool NvComputer::wake()
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
QByteArray wolPayload;
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
@@ -278,16 +293,25 @@ bool NvComputer::wake()
|
||||
return success;
|
||||
}
|
||||
|
||||
bool NvComputer::isReachableOverVpn()
|
||||
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||
{
|
||||
if (activeAddress.isNull()) {
|
||||
return false;
|
||||
NvAddress copyOfActiveAddress;
|
||||
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
|
||||
if (activeAddress.isNull()) {
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
|
||||
// Grab a copy of the active address to avoid having to hold
|
||||
// the computer lock while doing socket operations
|
||||
copyOfActiveAddress = activeAddress;
|
||||
}
|
||||
|
||||
QTcpSocket s;
|
||||
|
||||
s.setProxy(QNetworkProxy::NoProxy);
|
||||
s.connectToHost(activeAddress.address(), activeAddress.port());
|
||||
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port());
|
||||
if (s.waitForConnected(3000)) {
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
|
||||
@@ -308,48 +332,55 @@ bool NvComputer::isReachableOverVpn()
|
||||
if (nic.type() == QNetworkInterface::Virtual ||
|
||||
nic.type() == QNetworkInterface::Ppp) {
|
||||
// Treat PPP and virtual interfaces as likely VPNs
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||
// Treat MTUs under 1500 as likely VPNs
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||
// Treat point-to-point links as likely VPNs.
|
||||
// This check detects OpenVPN on Unix-like OSes.
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||
// ZeroTier interfaces always start with "ZeroTier"
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().contains("VPN")) {
|
||||
// This one is just a final heuristic if all else fails
|
||||
return true;
|
||||
// This one is just a final VPN heuristic if all else fails
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
// Didn't meet any of our VPN heuristics
|
||||
return false;
|
||||
// Didn't meet any of our VPN heuristics. Let's see if it's on-link.
|
||||
Q_ASSERT(addr.prefixLength() >= 0);
|
||||
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(addr.ip(), addr.prefixLength())) {
|
||||
return ReachabilityType::RI_LAN;
|
||||
}
|
||||
|
||||
// Default to unknown if nothing else matched
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No match found for address:" << s.localAddress();
|
||||
return false;
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
else {
|
||||
// If we fail to connect, just pretend that it's not a VPN
|
||||
return false;
|
||||
qWarning() << "Unable to check for reachability within 3 seconds";
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +406,7 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
|
||||
QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
QVector<NvAddress> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
@@ -406,7 +438,7 @@ QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
bool NvComputer::update(const NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
@@ -456,6 +488,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||
ASSIGN_IF_CHANGED(isNvidiaServerSoftware);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
|
||||
@@ -30,13 +30,20 @@ public:
|
||||
setRemoteAddress(QHostAddress);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
update(const NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
wake() const;
|
||||
|
||||
bool
|
||||
isReachableOverVpn();
|
||||
enum ReachabilityType
|
||||
{
|
||||
RI_UNKNOWN,
|
||||
RI_LAN,
|
||||
RI_VPN,
|
||||
};
|
||||
|
||||
ReachabilityType
|
||||
getActiveAddressReachability() const;
|
||||
|
||||
QVector<NvAddress>
|
||||
uniqueAddresses() const;
|
||||
@@ -83,6 +90,7 @@ public:
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
bool isNvidiaServerSoftware;
|
||||
|
||||
// Synchronization
|
||||
mutable QReadWriteLock lock;
|
||||
|
||||
@@ -551,7 +551,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
qWarning() << command << "request failed with error:" << reply->error();
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
|
||||
@@ -64,16 +64,6 @@ SystemProperties::SystemProperties()
|
||||
hasDiscordIntegration = false;
|
||||
#endif
|
||||
|
||||
// TODO: Do something smarter than this. We should be able to query
|
||||
// this from the decoder (or just try VIDEO_FORMAT_H265_MAIN10 and
|
||||
// fail if we don't get a hardware accelerated decoder).
|
||||
#ifdef Q_OS_DARWIN
|
||||
// HDR is supported by the VideoToolbox renderer
|
||||
supportsHdr = true;
|
||||
#else
|
||||
supportsHdr = false;
|
||||
#endif
|
||||
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
|
||||
// Populate data that requires talking to SDL. We do it all in one shot
|
||||
@@ -81,16 +71,9 @@ SystemProperties::SystemProperties()
|
||||
querySdlVideoInfo();
|
||||
|
||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
||||
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
}
|
||||
|
||||
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
return monitorDesktopResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
@@ -158,7 +141,7 @@ void SystemProperties::querySdlVideoInfoInternal()
|
||||
}
|
||||
}
|
||||
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
@@ -203,7 +186,6 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
return;
|
||||
}
|
||||
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
@@ -212,26 +194,8 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
SDL_DisplayMode desktopMode;
|
||||
int err;
|
||||
|
||||
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||
if (err == 0) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Skipping resolution over 8K: %dx%d",
|
||||
desktopMode.w, desktopMode.h);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ public:
|
||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
|
||||
signals:
|
||||
@@ -52,7 +51,6 @@ private:
|
||||
QString unmappedGamepads;
|
||||
int maximumStreamingFrameRate;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorDesktopResolutions;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
|
||||
@@ -164,8 +164,10 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" list List the available apps on a host\n"
|
||||
" quit Quit the currently running app\n"
|
||||
" stream Start streaming an app\n"
|
||||
" pair Pair a new host\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
);
|
||||
@@ -193,6 +195,10 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else if (action == "pair") {
|
||||
return PairRequested;
|
||||
} else if (action == "list") {
|
||||
return ListRequested;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +248,58 @@ QString QuitCommandLineParser::getHost() const
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
PairCommandLineParser::PairCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
PairCommandLineParser::~PairCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void PairCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Pair with the specified host."
|
||||
);
|
||||
parser.addPositionalArgument("pair", "pair host");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
parser.addValueOption("pin", "4 digit pairing PIN");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
m_PredefinedPin = parser.value("pin");
|
||||
if (!m_PredefinedPin.isEmpty() && m_PredefinedPin.length() != 4) {
|
||||
parser.showError("PIN must be 4 digits");
|
||||
}
|
||||
}
|
||||
|
||||
QString PairCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
QString PairCommandLineParser::getPredefinedPin() const
|
||||
{
|
||||
return m_PredefinedPin;
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
@@ -312,6 +370,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("keep-awake", "prevent display sleep while streaming");
|
||||
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());
|
||||
@@ -423,6 +482,9 @@ 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 --keep-awake and --no-keep-awake options
|
||||
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
|
||||
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
@@ -464,3 +526,62 @@ QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
ListCommandLineParser::ListCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
ListCommandLineParser::~ListCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void ListCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"List the available apps on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("list", "list available apps");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
parser.addFlagOption("csv", "Print as CSV with additional information");
|
||||
parser.addFlagOption("verbose", "Displays additional information");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
|
||||
m_PrintCSV = parser.isSet("csv");
|
||||
m_Verbose = parser.isSet("verbose");
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString ListCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
bool ListCommandLineParser::isPrintCSV() const
|
||||
{
|
||||
return m_PrintCSV;
|
||||
}
|
||||
|
||||
bool ListCommandLineParser::isVerbose() const
|
||||
{
|
||||
return m_Verbose;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public:
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
PairRequested,
|
||||
ListRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
@@ -35,6 +37,22 @@ private:
|
||||
QString m_Host;
|
||||
};
|
||||
|
||||
class PairCommandLineParser
|
||||
{
|
||||
public:
|
||||
PairCommandLineParser();
|
||||
virtual ~PairCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
QString getPredefinedPin() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
QString m_PredefinedPin;
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
@@ -55,3 +73,21 @@ private:
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
class ListCommandLineParser
|
||||
{
|
||||
public:
|
||||
ListCommandLineParser();
|
||||
virtual ~ListCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
bool isPrintCSV() const;
|
||||
bool isVerbose() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
bool m_PrintCSV;
|
||||
bool m_Verbose;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#include "listapps.h"
|
||||
|
||||
#include "backend/boxartmanager.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateListApp,
|
||||
StateSeekApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CLI main calls execute
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
|
||||
m_BoxArtManager = new BoxArtManager(q);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Establishing connection to PC...\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
fprintf(stderr, "%s\n", qPrintable(QString("Failed to connect to %1").arg(m_ComputerName)));
|
||||
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Loading app list...\n");
|
||||
}
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before retrieving games list.")
|
||||
.arg(event.computer->name)));
|
||||
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void printApps(QVector<NvApp> apps) {
|
||||
for (int i = 0; i < apps.length(); i++) {
|
||||
fprintf(stdout, "%s\n", qPrintable(apps[i].name));
|
||||
}
|
||||
}
|
||||
|
||||
void printAppsCSV(QVector<NvApp> apps) {
|
||||
fprintf(stdout, "Name, ID, HDR Support, App Collection Game, Hidden, Direct Launch, Boxart URL\n");
|
||||
for (int i = 0; i < apps.length(); i++) {
|
||||
printAppCSV(apps[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void printAppCSV(NvApp app) const
|
||||
{
|
||||
fprintf(stdout, "\"%s\",%d,%s,%s,%s,%s,\"%s\"\n", qPrintable(app.name),
|
||||
app.id,
|
||||
app.hdrSupported ? "true" : "false",
|
||||
app.isAppCollectorGame ? "true" : "false",
|
||||
app.hidden ? "true" : "false",
|
||||
app.directLaunch ? "true" : "false",
|
||||
qPrintable(m_BoxArtManager->loadBoxArt(m_Computer, app).toDisplayString()));
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
BoxArtManager *m_BoxArtManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
d->m_Arguments = arguments;
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "commandlineparser.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, ListCommandLineParser arguments, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "pair.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliPair
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StatePairing,
|
||||
StateFailure,
|
||||
StateComplete,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
Executed,
|
||||
PairingCompleted,
|
||||
Timedout,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliPair becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::pairingCompleted,
|
||||
q, &Launcher::onPairingCompleted);
|
||||
|
||||
// If we weren't provided a predefined PIN, generate one now
|
||||
if (m_PredefinedPin.isEmpty()) {
|
||||
m_PredefinedPin = m_ComputerManager->generatePinString();
|
||||
}
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("%1 is already paired").arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
else {
|
||||
Q_ASSERT(!m_PredefinedPin.isEmpty());
|
||||
|
||||
m_State = StatePairing;
|
||||
m_ComputerManager->pairHost(event.computer, m_PredefinedPin);
|
||||
emit q->pairing(event.computer->name, m_PredefinedPin);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when pairing operation completes
|
||||
case Event::PairingCompleted:
|
||||
if (m_State == StatePairing) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
m_State = StateComplete;
|
||||
emit q->success();
|
||||
}
|
||||
else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(event.errorMessage);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
QString m_ComputerName;
|
||||
QString m_PredefinedPin;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QString predefinedPin, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_PredefinedPin = predefinedPin;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Timedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onPairingCompleted(NvComputer* computer, QString error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::PairingCompleted);
|
||||
event.computer = computer;
|
||||
event.errorMessage = error;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class Session;
|
||||
class StreamingPreferences;
|
||||
|
||||
namespace CliPair
|
||||
{
|
||||
|
||||
class Event;
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QString predefinedPin,
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void pairing(QString pcName, QString pin);
|
||||
void failed(QString text);
|
||||
void success();
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onPairingCompleted(NvComputer *computer, QString error);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (NvApp app : m_Computer->appList) {
|
||||
for (const NvApp& app : m_Computer->appList) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,114 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="4.3.1" date="2022-11-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Fixed crash during specific patterns of audio packet loss</li>
|
||||
<li>Fixed detection of local streaming when using IPv6 addresses</li>
|
||||
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.3.0" date="2022-10-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved support for GeForce Experience 3.26</li>
|
||||
<li>Improved image quality and performance when recovering from video packet loss using HEVC</li>
|
||||
<li>Improved error concealment after audio packet loss</li>
|
||||
<li>Improved UI performance when paired with many hosts or hosts with large numbers of apps</li>
|
||||
<li>Pairing with Sunshine hosts is now supported even if the host is in a streaming session</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming at resolutions below 720x480 with GeForce Experience 3.26</li>
|
||||
<li>Fixed window position and size being lost when toggling between full-screen and windowed mode</li>
|
||||
<li>Fixed initial window size not always respecting the stream aspect ratio</li>
|
||||
<li>Fixed unexpected error when streaming at resolutions above 4096x4096 using Sunshine</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.1" date="2022-09-12">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming from hosts running Sunshine</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.0" date="2022-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for pairing via the terminal</li>
|
||||
<li>Added support for listing installed apps on a host via the terminal</li>
|
||||
<li>Improved handling of V-Sync on Wayland systems to reduce latency</li>
|
||||
<li>Improved performance using the legacy non-EGL VAAPI rendering code</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed native resolution detection on high DPI Wayland systems</li>
|
||||
<li>Fixed audio crackling with SDL 2.24.0 on systems using PulseAudio</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.1.0" date="2022-05-23">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Overlays are now supported on the non-EGL X11 VAAPI renderer (requires RGBA subpicture support in the VAAPI driver)</li>
|
||||
<li>Cursor lock can now be disabled when in full-screen mode</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>Moonlight can now reduce the client display resolution in full-screen mode if the current display mode doesn't have a high enough refresh rate for the selected stream frame rate</li>
|
||||
<li>When choosing a new client display resolution in full-screen mode, Moonlight will pick a display mode with the closest aspect ratio to the selected stream resolution</li>
|
||||
<li>Moonlight now defaults to borderless windowed mode on Wayland systems</li>
|
||||
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed missing toolbar buttons in certain languages</li>
|
||||
<li>Fixed some text on the settings page not updating when the language is changed</li>
|
||||
<li>Fixed a frame pacing bug that could cause excessive frame drops</li>
|
||||
<li>Fixed the "keep display awake" setting not being saved when Moonlight restarts</li>
|
||||
<li>Fixed black screen after unmaximizing the Moonlight window on some Linux systems</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.0.0" date="2022-04-02">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added a new key combo for locking the mouse cursor to the window (Ctrl+Alt+Shift+L)</li>
|
||||
<li>Added an option to avoid keeping the client display awake while streaming</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>HDR streaming is now a separate option rather than being part of the Video Codec override dropdown</li>
|
||||
<li>HDR streaming is no longer restricted to apps that are certified by NVIDIA as compatible with GameStream HDR. Some newer games may require an HDR display or EDID emulator dongle connected to your host PC for HDR options to be available.</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed loss of all audio if the active audio device is unplugged while streaming</li>
|
||||
<li>Fixed the GUI being unresponsive after manually quitting when connection to the host is lost</li>
|
||||
<li>Fixed a rare crash if a controller disconnects right as rumble is starting</li>
|
||||
<li>Fixed text rendering on the overlay UI when using SDL_ttf 2.0.18 on Linux</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.2.0" date="2022-01-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onPairing(pcName, pin) {
|
||||
stageLabel.text = qsTr("Pairing... Please enter '%1' on %2.").arg(pin).arg(pcName)
|
||||
}
|
||||
|
||||
function onFailed(message) {
|
||||
stageIndicator.visible = false
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onSuccess(appName) {
|
||||
stageIndicator.visible = false
|
||||
pairCompleteDialog.open()
|
||||
}
|
||||
|
||||
// Allow user to back out of pairing
|
||||
Keys.onEscapePressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
Keys.onBackPressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
Keys.onCancelPressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
|
||||
// Normally this is enabled by PcView, but we will won't
|
||||
// load PcView when streaming from the command-line.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.pairing.connect(onPairing)
|
||||
launcher.failed.connect(onFailed)
|
||||
launcher.success.connect(onSuccess)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
id: stageIndicator
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
onClosed: {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: pairCompleteDialog
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
text:qsTr("Pairing completed successfully")
|
||||
standardButtons: Dialog.Ok
|
||||
onClosed: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-14
@@ -232,22 +232,14 @@ CenteredGridView {
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = computerModel.generatePinString()
|
||||
var pin = computerModel.generatePinString()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
} else if (!model.online) {
|
||||
// Using open() here because it may be activated by keyboard
|
||||
|
||||
+187
-81
@@ -11,6 +11,8 @@ Flickable {
|
||||
id: settingsPage
|
||||
objectName: qsTr("Settings")
|
||||
|
||||
signal languageChanged()
|
||||
|
||||
boundsBehavior: Flickable.OvershootBounds
|
||||
|
||||
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
||||
@@ -95,50 +97,39 @@ Flickable {
|
||||
// Add native resolutions for all attached displays
|
||||
var done = false
|
||||
for (var displayIndex = 0; !done; displayIndex++) {
|
||||
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||
var screenRect;
|
||||
var screenRect = SystemProperties.getNativeResolution(displayIndex);
|
||||
|
||||
// Some platforms have different desktop resolutions
|
||||
// and native resolutions (like macOS with Retina displays)
|
||||
if (displayResIndex === 0) {
|
||||
screenRect = SystemProperties.getDesktopResolution(displayIndex)
|
||||
}
|
||||
else {
|
||||
screenRect = SystemProperties.getNativeResolution(displayIndex)
|
||||
}
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
done = true
|
||||
break
|
||||
}
|
||||
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
done = true
|
||||
var indexToAdd = 0
|
||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||
|
||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||
// Duplicate entry, skip
|
||||
indexToAdd = -1
|
||||
break
|
||||
}
|
||||
|
||||
var indexToAdd = 0
|
||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||
|
||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||
// Duplicate entry, skip
|
||||
indexToAdd = -1
|
||||
break
|
||||
}
|
||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||
// Candidate entrypoint after this entry
|
||||
indexToAdd = j + 1
|
||||
}
|
||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||
// Candidate entrypoint after this entry
|
||||
indexToAdd = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Insert this display's resolution if it's not a duplicate
|
||||
if (indexToAdd >= 0) {
|
||||
resolutionListModel.insert(indexToAdd,
|
||||
{
|
||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||
"video_width": ""+screenRect.width,
|
||||
"video_height": ""+screenRect.height,
|
||||
"is_custom": false
|
||||
})
|
||||
}
|
||||
// Insert this display's resolution if it's not a duplicate
|
||||
if (indexToAdd >= 0) {
|
||||
resolutionListModel.insert(indexToAdd,
|
||||
{
|
||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||
"video_width": ""+screenRect.width,
|
||||
"video_height": ""+screenRect.height,
|
||||
"is_custom": false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +336,7 @@ Flickable {
|
||||
maximumLength: 5
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_width
|
||||
validator: IntValidator{bottom:128; top:8192}
|
||||
validator: IntValidator{bottom:256; top:8192}
|
||||
focus: true
|
||||
|
||||
onTextChanged: {
|
||||
@@ -374,7 +365,7 @@ Flickable {
|
||||
maximumLength: 5
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_height
|
||||
validator: IntValidator{bottom:128; top:8192}
|
||||
validator: IntValidator{bottom:256; top:8192}
|
||||
|
||||
onTextChanged: {
|
||||
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||
@@ -465,6 +456,7 @@ Flickable {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
reinitialize()
|
||||
languageChanged.connect(reinitialize)
|
||||
}
|
||||
|
||||
id: fpsComboBox
|
||||
@@ -531,28 +523,53 @@ Flickable {
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
function createModel() {
|
||||
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||
|
||||
model.append({
|
||||
text: qsTr("Fullscreen"),
|
||||
val: StreamingPreferences.WM_FULLSCREEN
|
||||
})
|
||||
|
||||
model.append({
|
||||
text: qsTr("Borderless windowed"),
|
||||
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||
})
|
||||
|
||||
model.append({
|
||||
text: qsTr("Windowed"),
|
||||
val: StreamingPreferences.WM_WINDOWED
|
||||
})
|
||||
|
||||
|
||||
// Set the recommended option based on the OS
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var thisWm = model.get(i).val;
|
||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||
model.get(i).text += qsTr(" (Recommended)")
|
||||
model.move(i, 0, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
||||
// This is used on initialization and upon retranslation
|
||||
function reinitialize() {
|
||||
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;
|
||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||
windowModeListModel.get(i).text += qsTr(" (Recommended)")
|
||||
windowModeListModel.move(i, 0, 1);
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
model = createModel()
|
||||
currentIndex = 0
|
||||
|
||||
// Set the current value based on the saved preferences
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var thisWm = model.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
@@ -562,28 +579,18 @@ Flickable {
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
reinitialize()
|
||||
languageChanged.connect(reinitialize)
|
||||
}
|
||||
|
||||
id: windowModeComboBox
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
enabled: !SystemProperties.rendererAlwaysFullScreen
|
||||
hoverEnabled: true
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: windowModeListModel
|
||||
ListElement {
|
||||
text: qsTr("Fullscreen")
|
||||
val: StreamingPreferences.WM_FULLSCREEN
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("Borderless windowed")
|
||||
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("Windowed")
|
||||
val: StreamingPreferences.WM_WINDOWED
|
||||
}
|
||||
}
|
||||
onActivated: {
|
||||
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
|
||||
StreamingPreferences.windowMode = model.get(currentIndex).val
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
@@ -833,6 +840,34 @@ Flickable {
|
||||
text: "繁体字" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_HANT
|
||||
}
|
||||
ListElement {
|
||||
text: "Português" // Portuguese
|
||||
val: StreamingPreferences.LANG_PT
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Português do Brasil" // Brazilian Portuguese
|
||||
val: StreamingPreferences.LANG_PT_BR
|
||||
} */
|
||||
ListElement {
|
||||
text: "Ελληνικά" // Greek
|
||||
val: StreamingPreferences.LANG_EL
|
||||
}
|
||||
ListElement {
|
||||
text: "Italiano" // Italian
|
||||
val: StreamingPreferences.LANG_IT
|
||||
}
|
||||
/* ListElement {
|
||||
text: "हिन्दी, हिंदी" // Hindi
|
||||
val: StreamingPreferences.LANG_HI
|
||||
} */
|
||||
ListElement {
|
||||
text: "Język polski" // Polish
|
||||
val: StreamingPreferences.LANG_PL
|
||||
}
|
||||
ListElement {
|
||||
text: "Čeština" // Czech
|
||||
val: StreamingPreferences.LANG_CS
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
@@ -847,6 +882,9 @@ Flickable {
|
||||
// Force the back operation to pop any AppView pages that exist.
|
||||
// The AppView stops working after retranslate() for some reason.
|
||||
window.clearOnBack = true
|
||||
|
||||
// Signal other controls to adjust their text
|
||||
languageChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -933,6 +971,22 @@ Flickable {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Updates your Discord status to display the name of the game you're streaming.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: keepAwakeCheck
|
||||
width: parent.width
|
||||
text: qsTr("Keep the display awake while streaming")
|
||||
font.pointSize: 12
|
||||
checked: StreamingPreferences.keepAwake
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.keepAwake = checked
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Prevents the screensaver from starting or the display from going to sleep while streaming.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1252,6 +1306,7 @@ Flickable {
|
||||
|
||||
id: decoderComboBox
|
||||
textRole: "text"
|
||||
enabled: !enableHdr.checked
|
||||
model: ListModel {
|
||||
id: decoderListModel
|
||||
ListElement {
|
||||
@@ -1268,9 +1323,26 @@ Flickable {
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||
onActivated: {
|
||||
if (enabled) {
|
||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the state of the enableHdr checkbox changing
|
||||
onEnabledChanged: {
|
||||
if (enabled) {
|
||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||
}
|
||||
else {
|
||||
StreamingPreferences.videoDecoderSelection = StreamingPreferences.VDS_AUTO
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered && !enabled
|
||||
ToolTip.text: qsTr("Enabling HDR overrides manual decoder selections.")
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -1284,15 +1356,12 @@ Flickable {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.supportsHdr) {
|
||||
codecListModel.append({
|
||||
"text": qsTr("HEVC HDR (Experimental)"),
|
||||
"val": StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
});
|
||||
}
|
||||
|
||||
var saved_vcc = StreamingPreferences.videoCodecConfig
|
||||
|
||||
// Default to Automatic (relevant if HDR is enabled,
|
||||
// where we will match none of the codecs in the list)
|
||||
currentIndex = 0
|
||||
|
||||
for(var i = 0; i < codecListModel.count; i++) {
|
||||
var el_vcc = codecListModel.get(i).val;
|
||||
if (saved_vcc === el_vcc) {
|
||||
@@ -1300,11 +1369,13 @@ Flickable {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: codecComboBox
|
||||
textRole: "text"
|
||||
enabled: !enableHdr.checked
|
||||
model: ListModel {
|
||||
id: codecListModel
|
||||
ListElement {
|
||||
@@ -1319,12 +1390,47 @@ Flickable {
|
||||
text: qsTr("HEVC (H.265)")
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||
}
|
||||
/* HEVC HDR will be appended here */
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
if (enabled) {
|
||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
// This handles the state of the enableHdr checkbox changing
|
||||
onEnabledChanged: {
|
||||
if (enabled) {
|
||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
}
|
||||
else {
|
||||
StreamingPreferences.videoCodecConfig = StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered && !enabled
|
||||
ToolTip.text: qsTr("Enabling HDR overrides manual codec selections.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: enableHdr
|
||||
width: parent.width
|
||||
text: qsTr("Enable HDR (Experimental)")
|
||||
font.pointSize: 12
|
||||
enabled: SystemProperties.supportsHdr
|
||||
checked: enabled && StreamingPreferences.videoCodecConfig == StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
|
||||
// Updating StreamingPreferences.videoCodecConfig is handled above
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: enabled ?
|
||||
qsTr("The stream will be HDR-capable, but some games may require an HDR monitor on your host PC to enable HDR mode.")
|
||||
:
|
||||
qsTr("HDR streaming is not supported on this PC.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
|
||||
@@ -103,7 +103,10 @@ Item {
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sessionReadyForDeletion()
|
||||
{
|
||||
// Garbage collect the Session object since it's pretty heavyweight
|
||||
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||
session = null
|
||||
@@ -130,6 +133,7 @@ Item {
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||
|
||||
// Kick off the stream
|
||||
spinnerTimer.start()
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <random>
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
@@ -140,14 +138,9 @@ void ComputerModel::renameComputer(int computerIndex, QString name)
|
||||
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerModel::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
return m_ComputerManager->generatePinString();
|
||||
}
|
||||
|
||||
class DeferredTestConnectionTask : public QObject, public QRunnable
|
||||
|
||||
+17
-7
@@ -181,10 +181,20 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(url, objectName)
|
||||
// Workaround for lack of instanceof in Qt 5.9.
|
||||
//
|
||||
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
|
||||
function qmltypeof(obj, className) { // QtObject, string -> bool
|
||||
// className plus "(" is the class instance without modification
|
||||
// className plus "_QML" is the class instance with user-defined properties
|
||||
var str = obj.toString();
|
||||
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
|
||||
}
|
||||
|
||||
function navigateTo(url, objectType)
|
||||
{
|
||||
var existingItem = stackView.find(function(item, index) {
|
||||
return item.objectName === objectName
|
||||
return qmltypeof(item, objectType)
|
||||
})
|
||||
|
||||
if (existingItem !== null) {
|
||||
@@ -251,7 +261,7 @@ ApplicationWindow {
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: stackView.currentItem.objectName === qsTr("Settings")
|
||||
visible: qmltypeof(stackView.currentItem, "SettingsView")
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
@@ -261,7 +271,7 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
stackView.currentItem.objectName === qsTr("Settings")
|
||||
qmltypeof(stackView.currentItem, "SettingsView")
|
||||
|
||||
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
||||
|
||||
@@ -280,7 +290,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: stackView.currentItem.objectName === qsTr("Computers")
|
||||
visible: qmltypeof(stackView.currentItem, "PcView")
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
@@ -378,7 +388,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -390,7 +400,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+357
-263
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.
+263
-165
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+268
-170
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+253
-179
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.
+261
-167
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+269
-175
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+290
-192
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+262
-164
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
Binary file not shown.
+269
-171
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+255
-173
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+261
-167
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+254
-176
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+312
-230
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+262
-164
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+353
-255
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+265
-167
File diff suppressed because it is too large
Load Diff
+126
-34
@@ -22,20 +22,23 @@
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#if defined(Q_OS_WIN32)
|
||||
#include "antihookingprotection.h"
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include <openssl/ssl.h>
|
||||
#endif
|
||||
|
||||
#include "cli/listapps.h"
|
||||
#include "cli/quitstream.h"
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/pair.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
#include "utils.h"
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
#include "backend/autoupdatechecker.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/systemproperties.h"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
@@ -60,9 +63,11 @@
|
||||
static QElapsedTimer s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stdout);
|
||||
static QMutex s_LoggerLock;
|
||||
static bool s_SuppressVerboseOutput;
|
||||
#ifdef LOG_TO_FILE
|
||||
#define MAX_LOG_LINES 10000
|
||||
static int s_LogLinesWritten = 0;
|
||||
// Max log file size of 10 MB
|
||||
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
||||
static int s_LogBytesWritten = 0;
|
||||
static bool s_LogLimitReached = false;
|
||||
static QFile* s_LoggerFile;
|
||||
#endif
|
||||
@@ -75,7 +80,7 @@ void logToLoggerStream(QString& message)
|
||||
if (s_LogLimitReached) {
|
||||
return;
|
||||
}
|
||||
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
||||
else if (s_LogBytesWritten >= MAX_LOG_SIZE_BYTES) {
|
||||
s_LoggerStream << "Log size limit reached!";
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
s_LoggerStream << Qt::endl;
|
||||
@@ -86,7 +91,7 @@ void logToLoggerStream(QString& message)
|
||||
return;
|
||||
}
|
||||
else {
|
||||
s_LogLinesWritten++;
|
||||
s_LogBytesWritten += message.size();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -100,15 +105,27 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
|
||||
|
||||
switch (priority) {
|
||||
case SDL_LOG_PRIORITY_VERBOSE:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Verbose";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_DEBUG:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Debug";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_INFO:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Info";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_WARN:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Warn";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_ERROR:
|
||||
@@ -134,12 +151,21 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Debug";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Info";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Warning";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
@@ -166,13 +192,26 @@ void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||
if ((level & 0xFF) > av_log_get_level()) {
|
||||
return;
|
||||
}
|
||||
else if ((level & 0xFF) > AV_LOG_WARNING && s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to use the *previous* printPrefix value to determine whether to
|
||||
// print the prefix this time. av_log_format_line() will set the printPrefix
|
||||
// value to indicate whether the prefix should be printed *next time*.
|
||||
bool shouldPrefixThisMessage = printPrefix != 0;
|
||||
|
||||
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
if (shouldPrefixThisMessage) {
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
else {
|
||||
QString txt = QString(lineBuffer);
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -250,7 +289,6 @@ int main(int argc, char *argv[])
|
||||
QCoreApplication::setApplicationName("Moonlight");
|
||||
|
||||
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
|
||||
@@ -267,8 +305,8 @@ int main(int argc, char *argv[])
|
||||
#ifdef LOG_TO_FILE
|
||||
QDir tempDir(Path::getLogDir());
|
||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
||||
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
||||
if (s_LoggerFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl;
|
||||
s_LoggerStream.setDevice(s_LoggerFile);
|
||||
}
|
||||
#endif
|
||||
@@ -288,10 +326,9 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#if defined(Q_OS_WIN32)
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll on x86/x64 platforms by calling a dummy function.
|
||||
// AntiHooking.dll is not currently built for ARM64.
|
||||
// by ntdll on Win32 platforms by calling a dummy function.
|
||||
AntiHookingDummyImport();
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// Force libssl.so to be directly linked to our binary, so
|
||||
@@ -372,18 +409,6 @@ int main(int argc, char *argv[])
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||
// initializing the SDL video subsystem to have any effect.
|
||||
@@ -448,6 +473,10 @@ int main(int argc, char *argv[])
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
SDL_SetHint("SDL_APP_NAME", "Moonlight");
|
||||
|
||||
// We handle capturing the mouse ourselves when it leaves the window, so we don't need
|
||||
// SDL doing it for us behind our backs.
|
||||
SDL_SetHint("SDL_MOUSE_AUTO_CAPTURE", "0");
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||
// when throwing the exceptions, so we don't enable it for release builds out
|
||||
@@ -457,6 +486,47 @@ int main(int argc, char *argv[])
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
GlobalCommandLineParser::ParseResult commandLineParserResult = parser.parse(app.arguments());
|
||||
switch (commandLineParserResult) {
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
// Don't log to the console since it will jumble the command output
|
||||
s_SuppressVerboseOutput = true;
|
||||
#endif
|
||||
#ifdef Q_OS_WIN32
|
||||
// Attach to the console to be able to print output.
|
||||
// Since we're a /SUBSYSTEM:WINDOWS app, we won't be attached by default.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
HANDLE conOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (conOut != INVALID_HANDLE_VALUE && conOut != NULL) {
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
HANDLE conErr = GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (conErr != INVALID_HANDLE_VALUE && conErr != NULL) {
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
@@ -466,6 +536,7 @@ int main(int argc, char *argv[])
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||
qputenv("SDL_VIDEODRIVER", "x11");
|
||||
}
|
||||
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||
@@ -569,9 +640,9 @@ int main(int argc, char *argv[])
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
bool hasGUI = true;
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
switch (parser.parse(app.arguments())) {
|
||||
switch (commandLineParserResult) {
|
||||
case GlobalCommandLineParser::NormalStartRequested:
|
||||
initialView = "qrc:/gui/PcView.qml";
|
||||
break;
|
||||
@@ -596,14 +667,35 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::PairRequested:
|
||||
{
|
||||
initialView = "qrc:/gui/CliPair.qml";
|
||||
PairCommandLineParser pairParser;
|
||||
pairParser.parse(app.arguments());
|
||||
auto launcher = new CliPair::Launcher(pairParser.getHost(), pairParser.getPredefinedPin(), &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
{
|
||||
ListCommandLineParser listParser;
|
||||
listParser.parse(app.arguments());
|
||||
auto launcher = new CliListApps::Launcher(listParser.getHost(), listParser, &app);
|
||||
launcher->execute(new ComputerManager(&app));
|
||||
hasGUI = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
if (hasGUI) {
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
|
||||
+35
-47
@@ -11,6 +11,10 @@
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
// NOTE: This file MUST NOT include fcntl.h due to open() -> open64()
|
||||
// redirection that happens when _FILE_OFFSET_BITS=64!
|
||||
// See masterhook_internal.c for details.
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
@@ -35,14 +39,21 @@ struct stat64 g_DrmMasterStat;
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
int drmIsMaster(int fd)
|
||||
{
|
||||
// Detect master by attempting something that requires master.
|
||||
// This method is available in Mesa DRM since Feb 2019.
|
||||
return drmAuthMagic(fd, 0) != -EACCES;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -51,16 +62,16 @@ int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, __FUNCTION__))(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) {
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -69,53 +80,30 @@ int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, __FUNCTION__))(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)
|
||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int open64(const char *pathname, int flags, ...)
|
||||
{
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -124,7 +112,7 @@ int open(const char *pathname, int flags)
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, __FUNCTION__))(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
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file contains the private implementation of the open() hook
|
||||
// which must be in a separate compilation unit due to fcntl.h doing
|
||||
// unwanted redirection of open() to open64().
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// __OPEN_NEEDS_MODE is a glibc-ism, so define it ourselves for other libc
|
||||
#ifndef __OPEN_NEEDS_MODE
|
||||
#ifdef __O_TMPFILE
|
||||
# define __OPEN_NEEDS_MODE(oflag) \
|
||||
(((oflag) & O_CREAT) != 0 || ((oflag) & __O_TMPFILE) == __O_TMPFILE)
|
||||
#else
|
||||
# define __OPEN_NEEDS_MODE(oflag) (((oflag) & O_CREAT) != 0)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
extern int g_QtDrmMasterFd;
|
||||
extern struct stat64 g_DrmMasterStat;
|
||||
extern int g_SdlDrmMasterFd;
|
||||
|
||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||
{
|
||||
int fd;
|
||||
mode_t mode;
|
||||
|
||||
// Call the real thing to do the open operation
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
mode = va_arg(va, mode_t);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
mode = 0;
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(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);
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags);
|
||||
}
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
@@ -18,5 +18,6 @@
|
||||
<file>gui/NavigableMessageDialog.qml</file>
|
||||
<file>gui/NavigableDialog.qml</file>
|
||||
<file>gui/CenteredGridView.qml</file>
|
||||
<file>gui/CliPair.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[Platforms]
|
||||
WindowsArguments = darkmode=1
|
||||
@@ -49,6 +49,21 @@
|
||||
<file>languages/qml_uk.qm</file>
|
||||
<file>languages/qml_zh_Hant.ts</file>
|
||||
<file>languages/qml_zh_Hant.qm</file>
|
||||
<file>languages/qml_el.ts</file>
|
||||
<file>languages/qml_el.qm</file>
|
||||
<file>languages/qml_hi.ts</file>
|
||||
<file>languages/qml_hi.qm</file>
|
||||
<file>languages/qml_it.ts</file>
|
||||
<file>languages/qml_it.qm</file>
|
||||
<file>languages/qml_pt.ts</file>
|
||||
<file>languages/qml_pt.qm</file>
|
||||
<file>languages/qml_pl.ts</file>
|
||||
<file>languages/qml_pl.qm</file>
|
||||
<file>languages/qml_cs.ts</file>
|
||||
<file>languages/qml_cs.qm</file>
|
||||
<!-- Don't include pt_BR until it is more complete -->
|
||||
<!--file>languages/qml_pt_BR.qm</file-->
|
||||
<!--file>languages/qml_pt_BR.ts</file-->
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
@@ -59,5 +74,10 @@
|
||||
<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>
|
||||
<file alias="d3d11_vertex.fxc">shaders/d3d11_vertex.fxc</file>
|
||||
<file alias="d3d11_overlay_pixel.fxc">shaders/d3d11_overlay_pixel.fxc</file>
|
||||
<file alias="d3d11_genyuv_pixel.fxc">shaders/d3d11_genyuv_pixel.fxc</file>
|
||||
<file alias="d3d11_bt601lim_pixel.fxc">shaders/d3d11_bt601lim_pixel.fxc</file>
|
||||
<file alias="d3d11_bt2020lim_pixel.fxc">shaders/d3d11_bt2020lim_pixel.fxc</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "streamingpreferences.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTranslator>
|
||||
@@ -41,9 +42,10 @@
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
#define SER_KEEPAWAKE "keepawake"
|
||||
#define SER_LANGUAGE "language"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
#define CURRENT_DEFAULT_VER 2
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -68,7 +70,13 @@ void StreamingPreferences::reload()
|
||||
#ifdef Q_OS_DARWIN
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||
#else
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
|
||||
// Wayland doesn't support modesetting, so use fullscreen desktop mode.
|
||||
if (WMUtils::isRunningWayland()) {
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
else {
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
|
||||
}
|
||||
#endif
|
||||
|
||||
width = settings.value(SER_WIDTH, 1280).toInt();
|
||||
@@ -95,6 +103,7 @@ void StreamingPreferences::reload()
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
keepAwake = settings.value(SER_KEEPAWAKE, true).toBool();
|
||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
@@ -115,7 +124,7 @@ void StreamingPreferences::reload()
|
||||
|
||||
|
||||
// Perform default settings updates as required based on last default version
|
||||
if (defaultVer == 0) {
|
||||
if (defaultVer < 1) {
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
|
||||
if (windowMode == WindowMode::WM_FULLSCREEN) {
|
||||
@@ -123,6 +132,11 @@ void StreamingPreferences::reload()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (defaultVer < 2) {
|
||||
if (windowMode == WindowMode::WM_FULLSCREEN && WMUtils::isRunningWayland()) {
|
||||
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamingPreferences::retranslate()
|
||||
@@ -149,13 +163,13 @@ bool StreamingPreferences::retranslate()
|
||||
}
|
||||
|
||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||
qInfo() << "Successfully loaded translation for" << languageSuffix;
|
||||
|
||||
translator = newTranslator;
|
||||
QCoreApplication::installTranslator(translator);
|
||||
}
|
||||
else {
|
||||
qInfo() << "No translation available for " << languageSuffix;
|
||||
qInfo() << "No translation available for" << languageSuffix;
|
||||
delete newTranslator;
|
||||
}
|
||||
|
||||
@@ -215,6 +229,20 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
||||
return "uk";
|
||||
case LANG_ZH_HANT:
|
||||
return "zh_Hant";
|
||||
case LANG_PT:
|
||||
return "pt";
|
||||
case LANG_PT_BR:
|
||||
return "pt_BR";
|
||||
case LANG_EL:
|
||||
return "el";
|
||||
case LANG_IT:
|
||||
return "it";
|
||||
case LANG_HI:
|
||||
return "hi";
|
||||
case LANG_PL:
|
||||
return "pl";
|
||||
case LANG_CS:
|
||||
return "cs";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
@@ -257,6 +285,7 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||
settings.setValue(SER_KEEPAWAKE, keepAwake);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
|
||||
@@ -82,7 +82,14 @@ public:
|
||||
LANG_SV,
|
||||
LANG_TR,
|
||||
LANG_UK,
|
||||
LANG_ZH_HANT
|
||||
LANG_ZH_HANT,
|
||||
LANG_PT,
|
||||
LANG_PT_BR,
|
||||
LANG_EL,
|
||||
LANG_IT,
|
||||
LANG_HI,
|
||||
LANG_PL,
|
||||
LANG_CS,
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
@@ -123,6 +130,7 @@ 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 keepAwake MEMBER keepAwake NOTIFY keepAwakeChanged)
|
||||
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||
|
||||
@@ -152,6 +160,7 @@ public:
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
bool keepAwake;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
@@ -190,6 +199,7 @@ signals:
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysModeChanged();
|
||||
void keepAwakeChanged();
|
||||
void languageChanged();
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
fxc /T vs_4_0_level_9_3 /Fo d3d11_vertex.fxc d3d11_vertex.hlsl
|
||||
|
||||
fxc /T ps_4_0_level_9_3 /Fo d3d11_overlay_pixel.fxc d3d11_overlay_pixel.hlsl
|
||||
fxc /T ps_4_0_level_9_3 /Fo d3d11_genyuv_pixel.fxc d3d11_genyuv_pixel.hlsl
|
||||
fxc /T ps_4_0_level_9_3 /Fo d3d11_bt601lim_pixel.fxc d3d11_bt601lim_pixel.hlsl
|
||||
fxc /T ps_4_0_level_9_3 /Fo d3d11_bt2020lim_pixel.fxc d3d11_bt2020lim_pixel.hlsl
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
#include "d3d11_video_pixel_start.hlsli"
|
||||
|
||||
static const min16float3x3 cscMatrix =
|
||||
{
|
||||
1.1644, 1.1644, 1.1644,
|
||||
0.0, -0.1874, 2.1418,
|
||||
1.6781, -0.6505, 0.0,
|
||||
};
|
||||
|
||||
static const min16float3 offsets =
|
||||
{
|
||||
16.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0
|
||||
};
|
||||
|
||||
#include "d3d11_video_pixel_end.hlsli"
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
#include "d3d11_video_pixel_start.hlsli"
|
||||
|
||||
static const min16float3x3 cscMatrix =
|
||||
{
|
||||
1.1644, 1.1644, 1.1644,
|
||||
0.0, -0.3917, 2.0172,
|
||||
1.5960, -0.8129, 0.0,
|
||||
};
|
||||
|
||||
static const min16float3 offsets =
|
||||
{
|
||||
16.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0
|
||||
};
|
||||
|
||||
#include "d3d11_video_pixel_end.hlsli"
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
#include "d3d11_video_pixel_start.hlsli"
|
||||
|
||||
cbuffer CSC_CONST_BUF : register(b0)
|
||||
{
|
||||
min16float3x3 cscMatrix;
|
||||
min16float3 offsets;
|
||||
};
|
||||
|
||||
#include "d3d11_video_pixel_end.hlsli"
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
Texture2D<min16float4> theTexture : register(t0);
|
||||
SamplerState theSampler : register(s0);
|
||||
|
||||
struct ShaderInput
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD0;
|
||||
};
|
||||
|
||||
min16float4 main(ShaderInput input) : SV_TARGET
|
||||
{
|
||||
return theTexture.Sample(theSampler, input.tex);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
struct ShaderInput
|
||||
{
|
||||
float2 pos : POSITION;
|
||||
float2 tex : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct ShaderOutput
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD0;
|
||||
};
|
||||
|
||||
ShaderOutput main(ShaderInput input)
|
||||
{
|
||||
ShaderOutput output;
|
||||
output.pos = float4(input.pos, 0.0, 1.0);
|
||||
output.tex = input.tex;
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
min16float4 main(ShaderInput input) : SV_TARGET
|
||||
{
|
||||
min16float3 yuv = min16float3(luminancePlane.Sample(theSampler, input.tex),
|
||||
chrominancePlane.Sample(theSampler, input.tex));
|
||||
|
||||
// Subtract the YUV offset for limited vs full range
|
||||
yuv -= offsets;
|
||||
|
||||
// Multiply by the conversion matrix for this colorspace
|
||||
yuv = mul(yuv, cscMatrix);
|
||||
|
||||
return min16float4(yuv, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
Texture2D<min16float> luminancePlane : register(t0);
|
||||
Texture2D<min16float2> chrominancePlane : register(t1);
|
||||
SamplerState theSampler : register(s0);
|
||||
|
||||
struct ShaderInput
|
||||
{
|
||||
float4 pos : SV_POSITION;
|
||||
float2 tex : TEXCOORD0;
|
||||
};
|
||||
@@ -26,11 +26,10 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
want.format = AUDIO_S16;
|
||||
want.channels = opusConfig->channelCount;
|
||||
|
||||
// This is supposed to be a power of 2, but our
|
||||
// frames contain a non-power of 2 number of samples,
|
||||
// so the slop would require buffering another full frame.
|
||||
// Specifying non-Po2 seems to work for our supported platforms.
|
||||
want.samples = opusConfig->samplesPerFrame;
|
||||
// On PulseAudio systems, setting a value too small can cause underruns for other
|
||||
// applications sharing this output device. We impose a floor of 480 samples (10 ms)
|
||||
// to mitigate this issue.
|
||||
want.samples = SDL_max(480, opusConfig->samplesPerFrame);
|
||||
|
||||
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||
|
||||
@@ -88,6 +87,12 @@ void* SdlAudioRenderer::getAudioBuffer(int*)
|
||||
|
||||
bool SdlAudioRenderer::submitAudio(int bytesWritten)
|
||||
{
|
||||
// Our device may enter a permanent error status upon removal, so we need
|
||||
// to recreate the audio device to pick up the new default audio device.
|
||||
if (SDL_GetAudioDeviceStatus(m_AudioDevice) == SDL_AUDIO_STOPPED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesWritten == 0) {
|
||||
// Nothing to do
|
||||
return true;
|
||||
|
||||
@@ -259,7 +259,7 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
}
|
||||
|
||||
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
||||
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
|
||||
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG) && qgetenv("NO_GAMEPAD_QUIT") != "1") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit gamepad button combo");
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
m_MousePositionLock(0),
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_PointerRegionLockActive(false),
|
||||
m_PointerRegionLockToggledByUser(false),
|
||||
m_FakeCaptureActive(false),
|
||||
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
|
||||
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
|
||||
@@ -115,6 +117,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].keyCombo = KeyComboTogglePointerRegionLock;
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].keyCode = SDLK_l;
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].scanCode = SDL_SCANCODE_L;
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].enabled = true;
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
|
||||
@@ -333,27 +340,11 @@ void SdlInputHandler::updateKeyboardGrabState()
|
||||
// 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);
|
||||
}
|
||||
// On SDL 2.0.15+, we can get keyboard-only grab on Win32, X11, and Wayland.
|
||||
// SDL 2.0.18 adds keyboard grab on macOS (if built with non-AppStore APIs).
|
||||
SDL_SetWindowKeyboardGrab(m_Window, shouldGrab ? SDL_TRUE : SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the keyboard to leave the window
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
@@ -389,15 +380,6 @@ bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
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 ((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 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
|
||||
@@ -433,16 +415,11 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
else {
|
||||
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
|
||||
}
|
||||
|
||||
// Update mouse pointer region constraints
|
||||
updatePointerRegionLock();
|
||||
|
||||
// Now update the keyboard grab
|
||||
updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ public:
|
||||
|
||||
void updateKeyboardGrabState();
|
||||
|
||||
void updatePointerRegionLock();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
@@ -106,6 +108,7 @@ private:
|
||||
KeyComboToggleCursorHide,
|
||||
KeyComboToggleMinimize,
|
||||
KeyComboPasteText,
|
||||
KeyComboTogglePointerRegionLock,
|
||||
KeyComboMax
|
||||
};
|
||||
|
||||
@@ -156,6 +159,8 @@ private:
|
||||
SDL_atomic_t m_MousePositionUpdated;
|
||||
bool m_MouseWasInVideoRegion;
|
||||
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
|
||||
bool m_PointerRegionLockActive;
|
||||
bool m_PointerRegionLockToggledByUser;
|
||||
|
||||
int m_GamepadMask;
|
||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||
|
||||
@@ -126,6 +126,19 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||
break;
|
||||
}
|
||||
|
||||
case KeyComboTogglePointerRegionLock:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected pointer region lock toggle combo");
|
||||
m_PointerRegionLockActive = !m_PointerRegionLockActive;
|
||||
|
||||
// Remember that the user changed this manually, so we don't mess with it anymore
|
||||
// during windowed <-> full-screen transitions.
|
||||
m_PointerRegionLockToggledByUser = true;
|
||||
|
||||
// Apply the new region lock
|
||||
updatePointerRegionLock();
|
||||
break;
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
@@ -219,6 +219,12 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
event->preciseY = -event->preciseY;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// HACK: Clamp the scroll values on macOS to prevent OS scroll acceleration
|
||||
// from generating wild scroll deltas when scrolling quickly.
|
||||
event->preciseY = SDL_clamp(event->preciseY, -1.0f, 1.0f);
|
||||
#endif
|
||||
|
||||
LiSendHighResScrollEvent((short)(event->preciseY * 120)); // WHEEL_DELTA
|
||||
}
|
||||
#else
|
||||
@@ -228,6 +234,11 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
event->y = -event->y;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// See comment above
|
||||
event->y = SDL_clamp(event->y, -1, 1);
|
||||
#endif
|
||||
|
||||
LiSendScrollEvent((signed char)event->y);
|
||||
}
|
||||
#endif
|
||||
@@ -272,3 +283,54 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
void SdlInputHandler::updatePointerRegionLock()
|
||||
{
|
||||
// Pointer region lock is irrelevant in relative mouse mode
|
||||
if (SDL_GetRelativeMouseMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Our pointer lock behavior tracks with the fullscreen mode unless the user has
|
||||
// toggled it themselves using the keyboard shortcut. If that's the case, they
|
||||
// have full control over it and we don't touch it anymore.
|
||||
if (!m_PointerRegionLockToggledByUser) {
|
||||
// Lock the pointer in true full-screen mode and leave it unlocked in other modes
|
||||
m_PointerRegionLockActive = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
|
||||
// If region lock is enabled, grab the cursor so it can't accidentally leave our window.
|
||||
if (isCaptureActive() && m_PointerRegionLockActive) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
SDL_Rect src, dst;
|
||||
|
||||
src.x = src.y = 0;
|
||||
src.w = m_StreamWidth;
|
||||
src.h = m_StreamHeight;
|
||||
|
||||
dst.x = dst.y = 0;
|
||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||
|
||||
// Use the stream and window sizes to determine the video region
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
// SDL 2.0.18 lets us lock the cursor to a specific region
|
||||
SDL_SetWindowMouseRect(m_Window, &dst);
|
||||
#elif SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// SDL 2.0.15 only lets us lock the cursor to the whole window
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// Allow the cursor to leave the bounds of our video region or window
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
SDL_SetWindowMouseRect(m_Window, nullptr);
|
||||
#elif SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
|
||||
#else
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
+353
-195
@@ -22,7 +22,21 @@
|
||||
#define ICON_SIZE 64
|
||||
#endif
|
||||
|
||||
// HACK: Remove once proper Dark Mode support lands in SDL
|
||||
#ifdef Q_OS_WIN32
|
||||
#include <SDL_syswm.h>
|
||||
#include <dwmapi.h>
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_OLD
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE_OLD 19
|
||||
#endif
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
#define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100
|
||||
#define SDL_CODE_GAMECONTROLLER_RUMBLE 101
|
||||
|
||||
#include <openssl/rand.h>
|
||||
|
||||
@@ -34,6 +48,7 @@
|
||||
#include <QImage>
|
||||
#include <QGuiApplication>
|
||||
#include <QCursor>
|
||||
#include <QWindow>
|
||||
|
||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||
|
||||
@@ -45,7 +60,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clConnectionTerminated,
|
||||
Session::clLogMessage,
|
||||
Session::clRumble,
|
||||
Session::clConnectionStatusUpdate
|
||||
Session::clConnectionStatusUpdate,
|
||||
Session::clSetHdrMode,
|
||||
};
|
||||
|
||||
Session* Session::s_ActiveSession;
|
||||
@@ -95,6 +111,7 @@ void Session::clConnectionTerminated(int errorCode)
|
||||
emit s_ActiveSession->displayLaunchError(tr("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection."));
|
||||
break;
|
||||
|
||||
case ML_ERROR_PROTECTED_CONTENT:
|
||||
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("Something went wrong on your host PC when starting the stream.") + "\n\n" +
|
||||
@@ -102,6 +119,12 @@ void Session::clConnectionTerminated(int errorCode)
|
||||
tr("If the issue persists, try reinstalling your GPU drivers and GeForce Experience."));
|
||||
break;
|
||||
|
||||
case ML_ERROR_FRAME_CONVERSION:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("The host PC reported a fatal video encoding error.") + "\n\n" +
|
||||
tr("Try disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution."));
|
||||
break;
|
||||
|
||||
default:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("Connection terminated"));
|
||||
@@ -133,13 +156,15 @@ void Session::clLogMessage(const char* format, ...)
|
||||
|
||||
void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||
{
|
||||
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
|
||||
// but the stream has been stopped by the user. In this case, just discard the rumble.
|
||||
SDL_AtomicLock(&s_ActiveSession->m_InputHandlerLock);
|
||||
if (s_ActiveSession->m_InputHandler != nullptr) {
|
||||
s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||
}
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
|
||||
// We push an event for the main thread to handle in order to properly synchronize
|
||||
// with the removal of game controllers that could result in our game controller
|
||||
// going away during this callback.
|
||||
SDL_Event rumbleEvent = {};
|
||||
rumbleEvent.type = SDL_USEREVENT;
|
||||
rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE;
|
||||
rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
|
||||
rumbleEvent.user.data2 = (void*)(uintptr_t)((lowFreqMotor << 16) | highFreqMotor);
|
||||
SDL_PushEvent(&rumbleEvent);
|
||||
}
|
||||
|
||||
void Session::clConnectionStatusUpdate(int connectionStatus)
|
||||
@@ -175,6 +200,20 @@ void Session::clConnectionStatusUpdate(int connectionStatus)
|
||||
}
|
||||
}
|
||||
|
||||
void Session::clSetHdrMode(bool enabled)
|
||||
{
|
||||
// If we're in the process of recreating our decoder when we get
|
||||
// this callback, we'll drop it. The main thread will make the
|
||||
// callback when it finishes creating the new decoder.
|
||||
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||
if (decoder != nullptr) {
|
||||
decoder->setHdrMode(enabled);
|
||||
}
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
}
|
||||
}
|
||||
|
||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
|
||||
@@ -193,8 +232,13 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
params.window = window;
|
||||
params.enableVsync = enableVsync;
|
||||
params.enableFramePacing = enableFramePacing;
|
||||
params.testOnly = testOnly;
|
||||
params.vds = vds;
|
||||
|
||||
memset(¶ms.hdrMetadata, 0, sizeof(params.hdrMetadata));
|
||||
params.hdrMetadata.eotf = 2; // SMPTE ST 2084
|
||||
params.hdrMetadata.staticMetadataDescriptorId = 0; // Static Metadata Type 1
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync %s",
|
||||
enableVsync ? "enabled" : "disabled");
|
||||
@@ -261,14 +305,9 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
||||
// We need to destroy the decoder on the main thread to satisfy
|
||||
// some API constraints (like DXVA2). If we can't acquire it,
|
||||
// that means the decoder is about to be destroyed, so we can
|
||||
// safely return DR_OK and wait for m_NeedsIdr to be set by
|
||||
// safely return DR_OK and wait for the IDR frame request by
|
||||
// the decoder reinitialization code.
|
||||
|
||||
if (s_ActiveSession->getAndClearPendingIdrFrameStatus()) {
|
||||
// If we reset our decoder, we'll need to request an IDR frame
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||
if (decoder != nullptr) {
|
||||
@@ -289,39 +328,55 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
||||
}
|
||||
|
||||
void Session::getDecoderInfo(SDL_Window* window,
|
||||
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution)
|
||||
bool& isHardwareAccelerated, bool& isFullScreenOnly,
|
||||
bool& isHdrSupported, QSize& maxResolution)
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = isFullScreenOnly = false;
|
||||
// Try an HEVC Main10 decoder first to see if we have HDR support
|
||||
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
|
||||
window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
isHdrSupported = decoder->isHdrSupported();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
delete decoder;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
// HDR can only be supported by a hardware codec that can handle HEVC Main10.
|
||||
// If we made it this far, we don't have one, so HDR will not be available.
|
||||
isHdrSupported = false;
|
||||
|
||||
delete decoder;
|
||||
// Try a regular hardware accelerated HEVC decoder now
|
||||
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
|
||||
window, VIDEO_FORMAT_H265, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
delete decoder;
|
||||
|
||||
// If we don't get back a hardware H.264 decoder, see if we have a hardware
|
||||
// HEVC decoder. This can be the case on the Raspberry Pi with Full KMS
|
||||
// when not running in X11. Everything since Maxwell in 2014 can encode HEVC,
|
||||
// so we probably don't need to be concerned about a lack of fast H.264
|
||||
// decoding enough to bug the user about it every launch.
|
||||
if (!isHardwareAccelerated) {
|
||||
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
|
||||
window, VIDEO_FORMAT_H265, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
|
||||
delete decoder;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If we still didn't find a hardware decoder, try H.264 now.
|
||||
// This will fall back to software decoding, so it should always work.
|
||||
if (chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
delete decoder;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to find ANY working H.264 or HEVC decoder!");
|
||||
}
|
||||
|
||||
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
@@ -365,7 +420,29 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||
}
|
||||
|
||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||
{
|
||||
bool ok;
|
||||
|
||||
m_StreamConfig.colorSpace = qEnvironmentVariableIntValue("COLOR_SPACE_OVERRIDE", &ok);
|
||||
if (ok) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using colorspace override: %d",
|
||||
m_StreamConfig.colorSpace);
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||
}
|
||||
|
||||
m_StreamConfig.colorRange = qEnvironmentVariableIntValue("COLOR_RANGE_OVERRIDE", &ok);
|
||||
if (ok) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using color range override: %d",
|
||||
m_StreamConfig.colorRange);
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.colorRange = decoder->getDecoderColorRange();
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder->isAlwaysFullScreen()) {
|
||||
m_IsFullScreen = true;
|
||||
@@ -388,10 +465,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_AudioMuted(false),
|
||||
m_DisplayOriginX(0),
|
||||
m_DisplayOriginY(0),
|
||||
m_PendingWindowedTransition(false),
|
||||
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
||||
m_InputHandler(nullptr),
|
||||
m_InputHandlerLock(0),
|
||||
m_MouseEmulationRefCount(0),
|
||||
m_FlushingWindowEventsRef(0),
|
||||
m_AsyncConnectionSuccess(false),
|
||||
@@ -401,17 +476,6 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_AudioSampleCount(0),
|
||||
m_DropAudioEndTime(0)
|
||||
{
|
||||
SDL_AtomicSet(&m_NeedsIdr, 0);
|
||||
}
|
||||
|
||||
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
|
||||
// Use Session::exec() or DeferredSessionCleanupTask instead.
|
||||
Session::~Session()
|
||||
{
|
||||
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
||||
// and the object is deallocated.
|
||||
s_ActiveSessionSemaphore.acquire();
|
||||
s_ActiveSessionSemaphore.release();
|
||||
}
|
||||
|
||||
bool Session::initialize()
|
||||
@@ -607,8 +671,6 @@ void Session::emitLaunchWarning(QString text)
|
||||
|
||||
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;
|
||||
@@ -695,12 +757,8 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
// Turn HDR back off unless all criteria are met.
|
||||
m_StreamConfig.enableHdr = false;
|
||||
|
||||
// Check that the app supports HDR
|
||||
if (!m_App.hdrSupported) {
|
||||
emitLaunchWarning(tr("%1 doesn't support HDR10.").arg(m_App.name));
|
||||
}
|
||||
// Check that the server GPU supports HDR
|
||||
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HDR streaming. "
|
||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming."));
|
||||
}
|
||||
@@ -753,8 +811,13 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
emitLaunchWarning(tr("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this."));
|
||||
}
|
||||
|
||||
// NVENC will fail to initialize when using dimensions over 4096 and H.264.
|
||||
if (m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) {
|
||||
// NVENC will fail to initialize when any dimension exceeds 4096 using:
|
||||
// - H.264 on all versions of NVENC
|
||||
// - HEVC prior to Pascal
|
||||
//
|
||||
// However, if we aren't using Nvidia hosting software, don't assume anything about
|
||||
// encoding capabilities by using HEVC Main 10 support. It will likely be wrong.
|
||||
if ((m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) && m_Computer->isNvidiaServerSoftware) {
|
||||
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
|
||||
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0 || !(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
@@ -802,6 +865,9 @@ private:
|
||||
// Allow another session to start now that we're cleaned up
|
||||
Session::s_ActiveSession = nullptr;
|
||||
Session::s_ActiveSessionSemaphore.release();
|
||||
|
||||
// Notify that the session is ready to be cleaned up
|
||||
emit m_Session->readyForDeletion();
|
||||
}
|
||||
|
||||
void run() override
|
||||
@@ -850,12 +916,10 @@ void Session::getWindowDimensions(int& x, int& y,
|
||||
int& width, int& height)
|
||||
{
|
||||
int displayIndex = 0;
|
||||
bool fullScreen;
|
||||
|
||||
if (m_Window != nullptr) {
|
||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
SDL_assert(displayIndex >= 0);
|
||||
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
|
||||
}
|
||||
// Create our window on the same display that Qt's UI
|
||||
// was being displayed on.
|
||||
@@ -882,38 +946,32 @@ void Session::getWindowDimensions(int& x, int& y,
|
||||
i, SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
fullScreen = m_IsFullScreen;
|
||||
}
|
||||
|
||||
SDL_Rect usableBounds;
|
||||
if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) {
|
||||
width = usableBounds.w;
|
||||
height = usableBounds.h;
|
||||
}
|
||||
else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||
width = usableBounds.w;
|
||||
height = usableBounds.h;
|
||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||
// Don't use more than 80% of the display to leave room for system UI
|
||||
// and ensure the target size is not odd (otherwise one of the sides
|
||||
// of the image will have a one-pixel black bar next to it).
|
||||
SDL_Rect src, dst;
|
||||
src.x = src.y = dst.x = dst.y = 0;
|
||||
src.w = m_StreamConfig.width;
|
||||
src.h = m_StreamConfig.height;
|
||||
dst.w = ((int)SDL_ceilf(usableBounds.w * 0.80f) & ~0x1);
|
||||
dst.h = ((int)SDL_ceilf(usableBounds.h * 0.80f) & ~0x1);
|
||||
|
||||
if (m_Window != nullptr) {
|
||||
int top, left, bottom, right;
|
||||
// Scale the window size while preserving aspect ratio
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
|
||||
width -= left + right;
|
||||
height -= top + bottom;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to get window border size: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
// If the stream window can fit within the usable drawing area with 1:1
|
||||
// scaling, do that rather than filling the screen.
|
||||
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
}
|
||||
// If the stream window can fit within the usable drawing area with 1:1
|
||||
// scaling, do that rather than filling the screen.
|
||||
if (m_StreamConfig.width < dst.w && m_StreamConfig.height < dst.h) {
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
}
|
||||
else {
|
||||
width = dst.w;
|
||||
height = dst.h;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -939,7 +997,7 @@ void Session::updateOptimalWindowDisplayMode()
|
||||
// If this doesn't fit the selected resolution, use the native
|
||||
// resolution of the panel (unscaled).
|
||||
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
|
||||
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (!StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -959,6 +1017,9 @@ void Session::updateOptimalWindowDisplayMode()
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
|
||||
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Found display mode with desktop resolution: %dx%dx%d",
|
||||
mode.w, mode.h, mode.refresh_rate);
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
@@ -966,18 +1027,26 @@ void Session::updateOptimalWindowDisplayMode()
|
||||
}
|
||||
}
|
||||
|
||||
// For KMSDRM backends where we exclusively own the display, we should opt
|
||||
// to change the resolution if that allows us to hit the desired frame rate
|
||||
if (bestMode.refresh_rate == 0 && strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
|
||||
// If we didn't find a mode that matched the current resolution and
|
||||
// had a high enough refresh rate, start looking for lower resolution
|
||||
// modes that can meet the required refresh rate and minimum video
|
||||
// resolution. We will also try to pick a display mode that matches
|
||||
// aspect ratio closest to the video stream.
|
||||
if (bestMode.refresh_rate == 0) {
|
||||
float bestModeAspectRatio = 0;
|
||||
float videoAspectRatio = (float)m_ActiveVideoWidth / (float)m_ActiveVideoHeight;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected display mode: %dx%dx%d",
|
||||
mode.w, mode.h, mode.refresh_rate);
|
||||
float modeAspectRatio = (float)mode.w / (float)mode.h;
|
||||
if (mode.w >= m_ActiveVideoWidth && mode.h >= m_ActiveVideoHeight &&
|
||||
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Found display mode with video resolution: %dx%dx%d",
|
||||
mode.w, mode.h, mode.refresh_rate);
|
||||
if (mode.refresh_rate >= bestMode.refresh_rate &&
|
||||
(bestModeAspectRatio == 0 || fabs(videoAspectRatio - modeAspectRatio) <= fabs(videoAspectRatio - bestModeAspectRatio))) {
|
||||
bestMode = mode;
|
||||
bestModeAspectRatio = modeAspectRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -990,7 +1059,7 @@ void Session::updateOptimalWindowDisplayMode()
|
||||
// divide our FPS setting). We'll stick to the default in
|
||||
// this case.
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No matching refresh rate found; using desktop mode");
|
||||
"No matching display mode found; using desktop mode");
|
||||
bestMode = desktopMode;
|
||||
}
|
||||
|
||||
@@ -1009,28 +1078,37 @@ void Session::toggleFullscreen()
|
||||
{
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||
|
||||
if (fullScreen) {
|
||||
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.
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
#ifdef Q_OS_WIN32
|
||||
// Destroy the video decoder before toggling full-screen because D3D9 can try
|
||||
// to put the window back into full-screen before we've managed to destroy
|
||||
// the renderer. This leads to excessive flickering and can cause the window
|
||||
// decorations to get messed up as SDL and D3D9 fight over the window style.
|
||||
SDL_AtomicLock(&m_DecoderLock);
|
||||
delete m_VideoDecoder;
|
||||
m_VideoDecoder = nullptr;
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
#endif
|
||||
|
||||
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
}
|
||||
else {
|
||||
// Unconfine the cursor
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
// Actually enter/leave fullscreen
|
||||
SDL_SetWindowFullscreen(m_Window, fullScreen ? m_FullScreenFlag : 0);
|
||||
|
||||
SDL_SetWindowFullscreen(m_Window, 0);
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
|
||||
// Reposition the window when the resize is complete
|
||||
m_PendingWindowedTransition = true;
|
||||
#ifdef Q_OS_DARWIN
|
||||
// SDL on macOS has a bug that causes the window size to be reset to crazy
|
||||
// large dimensions when exiting out of true fullscreen mode. We can work
|
||||
// around the issue by manually resetting the position and size here.
|
||||
if (!fullScreen && m_FullScreenFlag == SDL_WINDOW_FULLSCREEN) {
|
||||
int x, y, width, height;
|
||||
getWindowDimensions(x, y, width, height);
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Input handler might need to start/stop keyboard grab after changing modes
|
||||
m_InputHandler->updateKeyboardGrabState();
|
||||
|
||||
// Input handler might need stop/stop mouse grab after changing modes
|
||||
m_InputHandler->updatePointerRegionLock();
|
||||
}
|
||||
|
||||
void Session::notifyMouseEmulationMode(bool enabled)
|
||||
@@ -1149,17 +1227,27 @@ bool Session::startConnectionAsync()
|
||||
m_Preferences->packetSize);
|
||||
}
|
||||
else {
|
||||
// isReachableOverVpn() does network I/O, so we only attempt to check
|
||||
// VPN reachability if we've already contacted the PC successfully
|
||||
if (m_Computer->isReachableOverVpn()) {
|
||||
// It looks like our route to this PC is over a VPN.
|
||||
// Use 1392 byte video packets by default
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
|
||||
// getActiveAddressReachability() does network I/O, so we only attempt to check
|
||||
// reachability if we've already contacted the PC successfully.
|
||||
switch (m_Computer->getActiveAddressReachability()) {
|
||||
case NvComputer::RI_LAN:
|
||||
// This address is on-link, so treat it as a local address
|
||||
// even if it's not in RFC 1918 space or it's an IPv6 address.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_LOCAL;
|
||||
break;
|
||||
case NvComputer::RI_VPN:
|
||||
// It looks like our route to this PC is over a VPN, so cap at 1024 bytes.
|
||||
// Treat it as remote even if the target address is in RFC 1918 address space.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
||||
m_StreamConfig.packetSize = 1024;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
default:
|
||||
// If we don't have reachability info, let moonlight-common-c decide.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,11 +1282,6 @@ void Session::flushWindowEvents()
|
||||
SDL_PushEvent(&flushEvent);
|
||||
}
|
||||
|
||||
bool Session::getAndClearPendingIdrFrameStatus()
|
||||
{
|
||||
return SDL_AtomicSet(&m_NeedsIdr, 0);
|
||||
}
|
||||
|
||||
class ExecThread : public QThread
|
||||
{
|
||||
public:
|
||||
@@ -1262,6 +1345,7 @@ void Session::execInternal()
|
||||
// called on the main thread.
|
||||
if (!initialize()) {
|
||||
emit sessionFinished(0);
|
||||
emit readyForDeletion();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1317,12 +1401,32 @@ void Session::execInternal()
|
||||
SDL_Delay(500);
|
||||
#endif
|
||||
|
||||
// Request at least 8 bits per color for GL
|
||||
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
|
||||
|
||||
// We always want a resizable window with High DPI enabled
|
||||
Uint32 defaultWindowFlags = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE;
|
||||
|
||||
// If we're starting in windowed mode and the Moonlight GUI is maximized,
|
||||
// match that with the streaming window.
|
||||
if (!m_IsFullScreen) {
|
||||
QWindowList windows = QGuiApplication::topLevelWindows();
|
||||
for (const QWindow* window : windows) {
|
||||
if (window->windowState() & Qt::WindowMaximized) {
|
||||
defaultWindowFlags |= SDL_WINDOW_MAXIMIZED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_Window = SDL_CreateWindow("Moonlight",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils::getPlatformWindowFlags());
|
||||
defaultWindowFlags | StreamUtils::getPlatformWindowFlags());
|
||||
if (!m_Window) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed with platform flags: %s",
|
||||
@@ -1333,7 +1437,7 @@ void Session::execInternal()
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
defaultWindowFlags);
|
||||
if (!m_Window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed: %s",
|
||||
@@ -1347,6 +1451,52 @@ void Session::execInternal()
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Remove once proper Dark Mode support lands in SDL
|
||||
#ifdef Q_OS_WIN32
|
||||
{
|
||||
BOOL darkModeEnabled = FALSE;
|
||||
|
||||
// Query whether dark mode is enabled for our Qt window (which tracks the OS dark mode state)
|
||||
QWindowList windows = QGuiApplication::topLevelWindows();
|
||||
for (const QWindow* window : windows) {
|
||||
if (SUCCEEDED(DwmGetWindowAttribute((HWND)window->winId(), DWMWA_USE_IMMERSIVE_DARK_MODE, &darkModeEnabled, sizeof(darkModeEnabled))) ||
|
||||
SUCCEEDED(DwmGetWindowAttribute((HWND)window->winId(), DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, &darkModeEnabled, sizeof(darkModeEnabled)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_SysWMinfo info;
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
if (SDL_GetWindowWMInfo(m_Window, &info) && info.subsystem == SDL_SYSWM_WINDOWS) {
|
||||
RECT clientRect;
|
||||
HBRUSH blackBrush;
|
||||
|
||||
// Draw a black background (otherwise our window will be bright white).
|
||||
//
|
||||
// TODO: Remove when SDL does this itself
|
||||
blackBrush = CreateSolidBrush(0);
|
||||
GetClientRect(info.info.win.window, &clientRect);
|
||||
FillRect(info.info.win.hdc, &clientRect, blackBrush);
|
||||
DeleteObject(blackBrush);
|
||||
|
||||
// If dark mode is enabled, propagate that to our SDL window
|
||||
if (darkModeEnabled) {
|
||||
if (FAILED(DwmSetWindowAttribute(info.info.win.window, DWMWA_USE_IMMERSIVE_DARK_MODE, &darkModeEnabled, sizeof(darkModeEnabled)))) {
|
||||
DwmSetWindowAttribute(info.info.win.window, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, &darkModeEnabled, sizeof(darkModeEnabled));
|
||||
}
|
||||
|
||||
// Toggle non-client rendering off and back on to ensure dark mode takes effect on Windows 10.
|
||||
// DWM doesn't seem to correctly invalidate the non-client area after enabling dark mode.
|
||||
DWMNCRENDERINGPOLICY ncPolicy = DWMNCRP_DISABLED;
|
||||
DwmSetWindowAttribute(info.info.win.window, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
|
||||
ncPolicy = DWMNCRP_ENABLED;
|
||||
DwmSetWindowAttribute(info.info.win.window, DWMWA_NCRENDERING_POLICY, &ncPolicy, sizeof(ncPolicy));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
m_InputHandler->setWindow(m_Window);
|
||||
|
||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||
@@ -1370,32 +1520,36 @@ void Session::execInternal()
|
||||
}
|
||||
#endif
|
||||
|
||||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (!m_IsFullScreen) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
// Update the window display mode based on our current monitor
|
||||
// for if/when we enter full-screen mode.
|
||||
updateOptimalWindowDisplayMode();
|
||||
|
||||
// We must set the size before the position because centering
|
||||
// won't work unless it knows the final size of the window.
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
|
||||
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||
// creation causes our window to be full screen for some reason
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
}
|
||||
else {
|
||||
// Update the window display mode based on our current monitor
|
||||
updateOptimalWindowDisplayMode();
|
||||
|
||||
// Enter full screen
|
||||
// Enter full screen if requested
|
||||
if (m_IsFullScreen) {
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
SDL_SysWMinfo info;
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
// Draw a black background again after entering full-screen to avoid visual artifacts
|
||||
// where the old window decorations were before.
|
||||
//
|
||||
// TODO: Remove when SDL does this itself
|
||||
if (SDL_GetWindowWMInfo(m_Window, &info) && info.subsystem == SDL_SYSWM_WINDOWS) {
|
||||
RECT clientRect;
|
||||
HBRUSH blackBrush;
|
||||
|
||||
blackBrush = CreateSolidBrush(0);
|
||||
GetClientRect(info.info.win.window, &clientRect);
|
||||
FillRect(info.info.win.hdc, &clientRect, blackBrush);
|
||||
DeleteObject(blackBrush);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool needsFirstEnterCapture = false;
|
||||
bool needsPostDecoderCreationCapture = false;
|
||||
bool ignoreDuplicateResizes = false;
|
||||
|
||||
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
|
||||
// event where it seems to work consistently on GNOME. For other platforms,
|
||||
@@ -1404,13 +1558,6 @@ void Session::execInternal()
|
||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
|
||||
needsFirstEnterCapture = true;
|
||||
|
||||
// As of SDL 2.0.20, we get duplicate SDL_WINDOWEVENT_SIZE_CHANGED events
|
||||
// when focus is lost and gained under GNOME Wayland. This causes us to
|
||||
// recreate our renderer needlessly. This is fixed in SDL 2.0.21.
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 21)
|
||||
ignoreDuplicateResizes = true;
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// X11/XWayland: Capture after decoder creation
|
||||
@@ -1423,8 +1570,10 @@ void Session::execInternal()
|
||||
// on macOS.
|
||||
SDL_StopTextInput();
|
||||
|
||||
// Disable the screen saver
|
||||
SDL_DisableScreenSaver();
|
||||
// Disable the screen saver if requested
|
||||
if (m_Preferences->keepAwake) {
|
||||
SDL_DisableScreenSaver();
|
||||
}
|
||||
|
||||
// Hide Qt's fake mouse cursor on EGLFS systems
|
||||
if (QGuiApplication::platformName() == "eglfs") {
|
||||
@@ -1448,7 +1597,6 @@ void Session::execInternal()
|
||||
// Hijack this thread to be the SDL main thread. We have to do this
|
||||
// because we want to suspend all Qt processing until the stream is over.
|
||||
SDL_Event event;
|
||||
Sint32 lastResizeW = -1, lastResizeH = -1;
|
||||
for (;;) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK)
|
||||
// SDL 2.0.18 has a proper wait event implementation that uses platform
|
||||
@@ -1490,7 +1638,9 @@ void Session::execInternal()
|
||||
case SDL_USEREVENT:
|
||||
switch (event.user.code) {
|
||||
case SDL_CODE_FRAME_READY:
|
||||
m_VideoDecoder->renderFrameOnMainThread();
|
||||
if (m_VideoDecoder != nullptr) {
|
||||
m_VideoDecoder->renderFrameOnMainThread();
|
||||
}
|
||||
break;
|
||||
case SDL_CODE_HIDE_CURSOR:
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
@@ -1504,6 +1654,11 @@ void Session::execInternal()
|
||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||
m_FlushingWindowEventsRef--;
|
||||
break;
|
||||
case SDL_CODE_GAMECONTROLLER_RUMBLE:
|
||||
m_InputHandler->rumble((unsigned short)(uintptr_t)event.user.data1,
|
||||
(unsigned short)((uintptr_t)event.user.data2 >> 16),
|
||||
(unsigned short)((uintptr_t)event.user.data2 & 0xFFFF));
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
}
|
||||
@@ -1543,24 +1698,27 @@ void Session::execInternal()
|
||||
// Check that the window display hasn't changed. If it has, we want
|
||||
// to recreate the decoder to allow it to adapt to the new display.
|
||||
// This will allow Pacer to pull the new display refresh rate.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
// On SDL 2.0.18+, there's an event for this specific situation
|
||||
if (event.window.event != SDL_WINDOWEVENT_DISPLAY_CHANGED) {
|
||||
break;
|
||||
}
|
||||
#else
|
||||
// Prior to SDL 2.0.18, we must check the display index for each window event
|
||||
if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Complete any repositioning that was deferred until
|
||||
// the resize from full-screen to windowed had completed.
|
||||
// If we try to do this immediately, the resize won't take effect
|
||||
// properly on Windows.
|
||||
if (m_PendingWindowedTransition) {
|
||||
m_PendingWindowedTransition = false;
|
||||
|
||||
int x, y, width, height;
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
#ifdef Q_OS_WIN32
|
||||
// We can get a resize event after being minimized. Recreating the renderer at that time can cause
|
||||
// us to start drawing on the screen even while our window is minimized. Minimizing on Windows also
|
||||
// moves the window to -32000, -32000 which can cause a false window display index change. Avoid
|
||||
// that whole mess by never recreating the decoder if we're minimized.
|
||||
else if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_MINIMIZED) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (m_FlushingWindowEventsRef > 0) {
|
||||
// Ignore window events for renderer reset if flushing
|
||||
@@ -1572,19 +1730,6 @@ void Session::execInternal()
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
if (ignoreDuplicateResizes && lastResizeW == event.window.data1 && lastResizeH == event.window.data2) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Dropping duplicate size changed event: %d %d",
|
||||
event.window.data1,
|
||||
event.window.data2);
|
||||
break;
|
||||
}
|
||||
|
||||
lastResizeW = event.window.data1;
|
||||
lastResizeH = event.window.data2;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Recreating renderer for window event: %d (%d %d)",
|
||||
event.window.event,
|
||||
@@ -1595,6 +1740,12 @@ void Session::execInternal()
|
||||
case SDL_RENDER_DEVICE_RESET:
|
||||
case SDL_RENDER_TARGETS_RESET:
|
||||
|
||||
if (event.type != SDL_WINDOWEVENT) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Recreating renderer by internal request: %d",
|
||||
event.type);
|
||||
}
|
||||
|
||||
SDL_AtomicLock(&m_DecoderLock);
|
||||
|
||||
// Destroy the old decoder
|
||||
@@ -1607,8 +1758,11 @@ void Session::execInternal()
|
||||
flushWindowEvents();
|
||||
|
||||
// Update the window display mode based on our current monitor
|
||||
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
updateOptimalWindowDisplayMode();
|
||||
// NB: Avoid a useless modeset by only doing this if it changed.
|
||||
if (currentDisplayIndex != SDL_GetWindowDisplayIndex(m_Window)) {
|
||||
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
updateOptimalWindowDisplayMode();
|
||||
}
|
||||
|
||||
// Now that the old decoder is dead, flush any events it may
|
||||
// have queued to reset itself (if this reset was the result
|
||||
@@ -1655,7 +1809,14 @@ void Session::execInternal()
|
||||
}
|
||||
|
||||
// Request an IDR frame to complete the reset
|
||||
SDL_AtomicSet(&m_NeedsIdr, 1);
|
||||
LiRequestIdrFrame();
|
||||
|
||||
// Set HDR mode. We may miss the callback if we're in the middle
|
||||
// of recreating our decoder at the time the HDR transition happens.
|
||||
m_VideoDecoder->setHdrMode(LiGetCurrentHostDisplayHdrMode());
|
||||
|
||||
// After a window resize, we need to reset the pointer lock region
|
||||
m_InputHandler->updatePointerRegionLock();
|
||||
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
break;
|
||||
@@ -1712,14 +1873,11 @@ DispatchDeferredCleanup:
|
||||
// Raise any keys that are still down
|
||||
m_InputHandler->raiseAllKeys();
|
||||
|
||||
// Destroy the input handler now. Any rumble callbacks that
|
||||
// occur after this point will be discarded. This must be destroyed
|
||||
// before allow the UI to continue execution or it could interfere
|
||||
// with SDLGamepadKeyNavigation.
|
||||
SDL_AtomicLock(&m_InputHandlerLock);
|
||||
// Destroy the input handler now. This must be destroyed
|
||||
// before allowwing the UI to continue execution or it could
|
||||
// interfere with SDLGamepadKeyNavigation.
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_AtomicUnlock(&m_InputHandlerLock);
|
||||
|
||||
// Destroy the decoder, since this must be done on the main thread
|
||||
// NB: This must happen before LiStopConnection() for pull-based
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user