mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-16 05:24:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547b8abc56 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 | ||
|
|
14a6a69dc0 | ||
|
|
3191182009 | ||
|
|
e210dc748f | ||
|
|
724dcb557f | ||
|
|
11d274ff07 | ||
|
|
ae246b1005 | ||
|
|
ca0250280a | ||
|
|
f49d6e15a4 | ||
|
|
7172cfd889 | ||
|
|
89e9652075 | ||
|
|
53098edc85 | ||
|
|
e9968fbab3 | ||
|
|
bc10fd0aba | ||
|
|
76b4922bbe | ||
|
|
8e790c4745 | ||
|
|
c331b180cb | ||
|
|
30e3b02867 | ||
|
|
bedea4a093 | ||
|
|
5372cfba20 | ||
|
|
19dac306d1 | ||
|
|
e9a98a3402 | ||
|
|
7020749987 | ||
|
|
2d62d090df | ||
|
|
e6f59062fa | ||
|
|
0d3092d62c | ||
|
|
af0921c5b2 | ||
|
|
137454beff | ||
|
|
a0cc93b813 | ||
|
|
ff516f63b4 | ||
|
|
68acb78ae6 | ||
|
|
949aaaff2a | ||
|
|
390544cd9a | ||
|
|
766a923f86 | ||
|
|
202ce14afa | ||
|
|
8ff9d8123d | ||
|
|
2715367411 | ||
|
|
44b61d9252 | ||
|
|
565bba6e55 | ||
|
|
2ecfb7af64 | ||
|
|
998d3b2cc1 | ||
|
|
c923d643b9 | ||
|
|
bc39c721e3 | ||
|
|
5096ff6496 | ||
|
|
a11f623b17 | ||
|
|
f7ffb30bc6 | ||
|
|
c7ddb54e67 | ||
|
|
c4108feb15 | ||
|
|
47cad71fcd | ||
|
|
0b56b1685a | ||
|
|
c414e1a168 | ||
|
|
22bebb6bbc | ||
|
|
c3f9742bb6 | ||
|
|
3defccb6e2 | ||
|
|
1197b17509 | ||
|
|
265e28e7c3 | ||
|
|
9c758f1628 | ||
|
|
5f4975ca5c | ||
|
|
e5bcc793e5 | ||
|
|
cd5b0e74e3 | ||
|
|
3162d2c17d | ||
|
|
6a54fb649c | ||
|
|
8031ed7fe1 | ||
|
|
dcbf5affda | ||
|
|
027c8dcd41 | ||
|
|
94b46a2173 | ||
|
|
b66f7f6c8b | ||
|
|
355e4a5c67 | ||
|
|
1e7cb7f13e | ||
|
|
d58837421f | ||
|
|
19d5358b97 | ||
|
|
a6551c320d | ||
|
|
59a73817c8 | ||
|
|
1231f3eeb1 | ||
|
|
cd40c1dacd | ||
|
|
33286a5bde | ||
|
|
b61c35aff9 | ||
|
|
2a3e1ba62e | ||
|
|
d95f3277b2 | ||
|
|
e0a2c8cb8a | ||
|
|
0aa46a57fe | ||
|
|
53e3cd143e | ||
|
|
8381039f97 | ||
|
|
a91c61e5e2 | ||
|
|
f90efd7e38 | ||
|
|
fab142dd38 | ||
|
|
125d546cea | ||
|
|
179f7ecc23 | ||
|
|
0fc30a70ca | ||
|
|
f81509a892 | ||
|
|
fc4488f17f | ||
|
|
2748f3d22e | ||
|
|
e0d42cbe9c | ||
|
|
1780d9417f | ||
|
|
5f182b1e62 | ||
|
|
80cee8367f | ||
|
|
7325d5657d | ||
|
|
72182c7caa | ||
|
|
d7ca3801be | ||
|
|
2ef714f87b | ||
|
|
46940eb15a | ||
|
|
3485983553 | ||
|
|
861ebc151a | ||
|
|
ae5df938b6 | ||
|
|
e224a7f0c7 | ||
|
|
aa4684077d | ||
|
|
193e196c46 | ||
|
|
6e7a42065c | ||
|
|
2c987d2912 | ||
|
|
19f5dc12fb | ||
|
|
38b59aa1a9 | ||
|
|
201b59cd4c | ||
|
|
de3afe27c1 | ||
|
|
7bee8e73f0 | ||
|
|
17e0fbbaf8 | ||
|
|
ec486e89e4 | ||
|
|
ec3b000264 | ||
|
|
90a513dc3d | ||
|
|
32a74e3416 | ||
|
|
1d90bf9230 |
@@ -1,10 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Ask for help on Discord before reporting a bug
|
||||
about: Follow the troubleshooting guide before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||
|
||||
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
@@ -26,14 +28,21 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||
- Do you have any gamepads connected to your host PC directly?
|
||||
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||
|
||||
**Client PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- Moonlight Version: [e.g. v0.9.0]
|
||||
- GPU: [e.g. Intel HD Graphics 520]
|
||||
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||
|
||||
**Moonlight Logs (please attach)**
|
||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||
|
||||
+5
-1
@@ -9,7 +9,11 @@
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/andrewrk/libsoundio.git
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
[submodule "libs"]
|
||||
path = libs
|
||||
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
|
||||
shallow = true
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
[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 also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
@@ -15,6 +16,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 7.1 surround sound audio support
|
||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||
- Direct touchscreen mouse control
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Gamepad support with force feedback for up to 4 players
|
||||
@@ -27,19 +29,24 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||
|
||||
#### Special Thanks
|
||||
|
||||
[](https://cloudsmith.com)
|
||||
|
||||
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||
|
||||
## Building
|
||||
|
||||
### Windows Build Requirements
|
||||
* Qt 5.12.4 SDK or later
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
|
||||
* Select MSVC Desktop toolchain during Qt installation
|
||||
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](https://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
|
||||
### macOS Build Requirements
|
||||
* Qt 5.14 SDK or later
|
||||
* Qt 5.15 SDK or later
|
||||
* macOS High Sierra (10.13) or later
|
||||
* Xcode 11
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
@@ -47,8 +54,10 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
* GCC or Clang
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
|
||||
### Steam Link Build Requirements
|
||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||
@@ -62,10 +71,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.11</string>
|
||||
<string>10.13</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: 7518c26389...376c576317
+36
-11
@@ -33,17 +33,16 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += \
|
||||
$$PWD/../libs/windows/include \
|
||||
$$(DXSDK_DIR)/Include
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
|
||||
@@ -102,7 +101,7 @@ unix:!macx {
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
win32:!winrt {
|
||||
@@ -132,8 +131,11 @@ SOURCES += \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
settings/mappingfetcher.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input/abstouch.cpp \
|
||||
streaming/input/clipboard.cpp \
|
||||
streaming/input/gamepad.cpp \
|
||||
streaming/input/input.cpp \
|
||||
streaming/input/keyboard.cpp \
|
||||
@@ -155,6 +157,8 @@ SOURCES += \
|
||||
|
||||
HEADERS += \
|
||||
backend/nvapp.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
@@ -249,6 +253,12 @@ libdrm {
|
||||
DEFINES += HAVE_DRM
|
||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
@@ -263,7 +273,7 @@ config_EGL {
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
|
||||
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
LIBS += -lSLVideo -lSLAudio
|
||||
|
||||
SOURCES += \
|
||||
@@ -306,11 +316,24 @@ discord-rpc {
|
||||
LIBS += -ldiscord-rpc
|
||||
DEFINES += HAVE_DISCORD
|
||||
}
|
||||
embedded {
|
||||
message(Embedded build)
|
||||
|
||||
DEFINES += EMBEDDED_BUILD
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
languages/qml_ru.ts \
|
||||
languages/qml_es.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -348,11 +371,13 @@ INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
!winrt {
|
||||
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
|
||||
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
|
||||
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
|
||||
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||
@@ -39,7 +39,13 @@ void AutoUpdateChecker::start()
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||
m_Nam.get(QNetworkRequest(url));
|
||||
QNetworkRequest request(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -57,18 +63,53 @@ QString AutoUpdateChecker::getPlatform()
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||
// to be consistent (and not require another entry in the manifest).
|
||||
return QStringLiteral("osx");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||
for (int i = 0;; i++) {
|
||||
int v1Val = 0;
|
||||
int v2Val = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < version1.count()) {
|
||||
v1Val = version1[i];
|
||||
}
|
||||
if (i < version2.count()) {
|
||||
v2Val = version2[i];
|
||||
}
|
||||
if (i >= version1.count() && i >= version2.count()) {
|
||||
// Equal versions
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (v1Val < v2Val) {
|
||||
return -1;
|
||||
}
|
||||
else if (v1Val > v2Val) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QTextStream stream(reply);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
// Read all data and queue the reply for deletion
|
||||
QString jsonString = stream.readAll();
|
||||
@@ -108,46 +149,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == getPlatform()) {
|
||||
|
||||
// Check the kernel version minimum if one exists
|
||||
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||
QVector<int> requiredVersionQuad;
|
||||
QVector<int> actualVersionQuad;
|
||||
|
||||
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||
QString actualVersion = QSysInfo::kernelVersion();
|
||||
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||
|
||||
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Found update manifest match for current platform";
|
||||
|
||||
QString latestVersion = updateObj["version"].toString();
|
||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||
|
||||
QVector<int> latestVersionQuad;
|
||||
|
||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int latestVer = 0;
|
||||
int currentVer = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < latestVersionQuad.count()) {
|
||||
latestVer = latestVersionQuad[i];
|
||||
}
|
||||
if (i < m_CurrentVersionQuad.count()) {
|
||||
currentVer = m_CurrentVersionQuad[i];
|
||||
}
|
||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentVer < latestVer) {
|
||||
qDebug() << "Update available";
|
||||
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (currentVer > latestVer) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||
if (res < 0) {
|
||||
// m_CurrentVersionQuad < latestVersionQuad
|
||||
qDebug() << "Update available";
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (res > 0) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
else {
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -155,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform: "
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
||||
qWarning() << "No entry in update manifest found for current platform:"
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
|
||||
@@ -20,6 +20,8 @@ private slots:
|
||||
private:
|
||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||
|
||||
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
|
||||
@@ -46,8 +46,8 @@ public:
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||
}
|
||||
|
||||
signals:
|
||||
|
||||
@@ -146,7 +146,8 @@ private:
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr)
|
||||
m_MdnsBrowser(nullptr),
|
||||
m_CompatFetcher(nullptr)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
@@ -159,12 +160,15 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Fetch latest compatibility data asynchronously
|
||||
m_CompatFetcher.start();
|
||||
|
||||
// To quit in a timely manner, we must block additional requests
|
||||
// after we receive the aboutToQuit() signal. This is neccessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
@@ -302,8 +306,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
|
||||
if (!pollingEntry->isActive()) {
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||
this, &ComputerManager::handleComputerStateChanged);
|
||||
pollingEntry->setActiveThread(thread);
|
||||
thread->start();
|
||||
}
|
||||
@@ -337,7 +341,6 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +421,7 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
{
|
||||
{
|
||||
QWriteLocker(&computer->lock);
|
||||
QWriteLocker lock(&computer->lock);
|
||||
|
||||
computer->name = name;
|
||||
computer->hasCustomName = true;
|
||||
@@ -536,8 +539,8 @@ private:
|
||||
}
|
||||
if (e.getStatusCode() == 599) {
|
||||
// 599 is a special code we make a custom message for
|
||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||
emit quitAppFailed(tr("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game."));
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "nvcomputer.h"
|
||||
#include "nvpairingmanager.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
#include <qmdnsengine/cache.h>
|
||||
@@ -70,7 +71,7 @@ private:
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
@@ -214,4 +215,5 @@ private:
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
};
|
||||
|
||||
@@ -143,7 +143,10 @@ QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||
// the Steam Link hardware.
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#define SER_APPHDR "hdr"
|
||||
#define SER_APPCOLLECTOR "appcollector"
|
||||
#define SER_HIDDEN "hidden"
|
||||
#define SER_DIRECTLAUNCH "directlaunch"
|
||||
|
||||
NvApp::NvApp(QSettings& settings)
|
||||
{
|
||||
@@ -13,6 +14,7 @@ NvApp::NvApp(QSettings& settings)
|
||||
hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
|
||||
hidden = settings.value(SER_HIDDEN).toBool();
|
||||
directLaunch = settings.value(SER_DIRECTLAUNCH).toBool();
|
||||
}
|
||||
|
||||
void NvApp::serialize(QSettings& settings) const
|
||||
@@ -22,4 +24,5 @@ void NvApp::serialize(QSettings& settings) const
|
||||
settings.setValue(SER_APPHDR, hdrSupported);
|
||||
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
|
||||
settings.setValue(SER_HIDDEN, hidden);
|
||||
settings.setValue(SER_DIRECTLAUNCH, directLaunch);
|
||||
}
|
||||
|
||||
+3
-1
@@ -14,7 +14,8 @@ public:
|
||||
name == other.name &&
|
||||
hdrSupported == other.hdrSupported &&
|
||||
isAppCollectorGame == other.isAppCollectorGame &&
|
||||
hidden == other.hidden;
|
||||
hidden == other.hidden &&
|
||||
directLaunch == other.directLaunch;
|
||||
}
|
||||
|
||||
bool operator!=(const NvApp& other) const
|
||||
@@ -35,6 +36,7 @@ public:
|
||||
bool hdrSupported = false;
|
||||
bool isAppCollectorGame = false;
|
||||
bool hidden = false;
|
||||
bool directLaunch = false;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
@@ -30,6 +31,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
@@ -49,6 +51,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
this->gpuModel = nullptr;
|
||||
this->isSupportedServerVersion = true;
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings) const
|
||||
@@ -98,7 +101,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
for (const QString& macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
@@ -142,6 +145,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
this->activeAddress = address;
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
@@ -317,6 +321,7 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
for (NvApp& newApp : newAppList) {
|
||||
if (existingApp.id == newApp.id) {
|
||||
newApp.hidden = existingApp.hidden;
|
||||
newApp.directLaunch = existingApp.directLaunch;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,6 +411,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
|
||||
@@ -65,6 +65,7 @@ public:
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
bool isSupportedServerVersion;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
|
||||
+30
-18
@@ -67,7 +67,8 @@ NvHTTP::parseQuad(QString quad)
|
||||
}
|
||||
|
||||
QStringList parts = quad.split(".");
|
||||
for (int i = 0; i < 4; i++)
|
||||
ret.reserve(parts.length());
|
||||
for (int i = 0; i < parts.length(); i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
@@ -233,17 +234,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
|
||||
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (name == "DisplayMode") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("DisplayMode")) {
|
||||
modes.append(NvDisplayMode());
|
||||
}
|
||||
else if (name == "Width") {
|
||||
else if (name == QString("Width")) {
|
||||
modes.last().width = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (name == "Height") {
|
||||
else if (name == QString("Height")) {
|
||||
modes.last().height = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (name == "RefreshRate") {
|
||||
else if (name == QString("RefreshRate")) {
|
||||
modes.last().refreshRate = xmlReader.readElementText().toInt();
|
||||
}
|
||||
}
|
||||
@@ -266,8 +267,8 @@ NvHTTP::getAppList()
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (name == "App") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("App")) {
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
@@ -276,16 +277,16 @@ NvHTTP::getAppList()
|
||||
}
|
||||
apps.append(NvApp());
|
||||
}
|
||||
else if (name == "AppTitle") {
|
||||
else if (name == QString("AppTitle")) {
|
||||
apps.last().name = xmlReader.readElementText();
|
||||
}
|
||||
else if (name == "ID") {
|
||||
else if (name == QString("ID")) {
|
||||
apps.last().id = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (name == "IsHdrSupported") {
|
||||
else if (name == QString("IsHdrSupported")) {
|
||||
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
||||
}
|
||||
else if (name == "IsAppCollectorGame") {
|
||||
else if (name == QString("IsAppCollectorGame")) {
|
||||
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
|
||||
}
|
||||
}
|
||||
@@ -301,7 +302,7 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
|
||||
while (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == "root")
|
||||
if (xmlReader.name() == QString("root"))
|
||||
{
|
||||
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
|
||||
// QString::toInt() will fail in that case, so use QString::toUInt()
|
||||
@@ -323,7 +324,7 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
// Special case handling an audio capture error which GFE doesn't
|
||||
// provide any useful status message for.
|
||||
statusCode = 418;
|
||||
statusMessage = "Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
|
||||
statusMessage = tr("Missing audio capture device. Reinstalling GeForce Experience should resolve this error.");
|
||||
}
|
||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||
}
|
||||
@@ -391,7 +392,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto error : errors) {
|
||||
for (const QSslError& error : errors) {
|
||||
if (m_ServerCert != error.certificate()) {
|
||||
ignoreErrors = false;
|
||||
break;
|
||||
@@ -414,7 +415,13 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
ret = stream.readAll();
|
||||
delete reply;
|
||||
|
||||
@@ -444,6 +451,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
@@ -456,10 +468,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
|
||||
@@ -215,7 +215,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
@@ -238,17 +238,18 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
serverCert = QSslCertificate(serverCertStr);
|
||||
if (serverCert.isNull()) {
|
||||
Q_ASSERT(!serverCert.isNull());
|
||||
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
|
||||
if (unverifiedServerCert.isNull()) {
|
||||
Q_ASSERT(!unverifiedServerCert.isNull());
|
||||
|
||||
qCritical() << "Failed to parse plaincert";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
// Pin this cert for TLS
|
||||
m_Http.setServerCert(serverCert);
|
||||
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
|
||||
// the cert into the NvComputer object and persist it.
|
||||
m_Http.setServerCert(unverifiedServerCert);
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||
@@ -353,5 +354,6 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
serverCert = std::move(unverifiedServerCert);
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
|
||||
@@ -6,25 +6,57 @@
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
SystemProperties::SystemProperties()
|
||||
{
|
||||
versionString = QString(VERSION_STR);
|
||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||
{
|
||||
USHORT processArch, machineArch;
|
||||
|
||||
// Use IsWow64Process2 on TH2 and later, because it supports ARM64
|
||||
auto fnIsWow64Process2 = (decltype(IsWow64Process2)*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "IsWow64Process2");
|
||||
if (fnIsWow64Process2 != nullptr && fnIsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
|
||||
switch (machineArch) {
|
||||
case IMAGE_FILE_MACHINE_I386:
|
||||
nativeArch = "i386";
|
||||
break;
|
||||
case IMAGE_FILE_MACHINE_AMD64:
|
||||
nativeArch = "x86_64";
|
||||
break;
|
||||
case IMAGE_FILE_MACHINE_ARM64:
|
||||
nativeArch = "arm64";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isWow64 = nativeArch != QSysInfo::buildCpuArchitecture();
|
||||
}
|
||||
#else
|
||||
isWow64 = false;
|
||||
#endif
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
if (nativeArch == "i386") {
|
||||
friendlyNativeArchName = "x86";
|
||||
}
|
||||
else if (nativeArch == "x86_64") {
|
||||
friendlyNativeArchName = "x64";
|
||||
}
|
||||
else {
|
||||
friendlyNativeArchName = nativeArch.toUpper();
|
||||
}
|
||||
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasWindowManager;
|
||||
#else
|
||||
hasBrowser = false;
|
||||
#endif
|
||||
hasBrowser = hasDesktopEnvironment;
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
hasDiscordIntegration = true;
|
||||
@@ -55,14 +87,38 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
{
|
||||
public:
|
||||
QuerySdlVideoThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->querySdlVideoInfoInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
hasHardwareAcceleration = false;
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
QuerySdlVideoThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
querySdlVideoInfoInternal();
|
||||
}
|
||||
}
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
void SystemProperties::querySdlVideoInfoInternal()
|
||||
{
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -71,6 +127,78 @@ void SystemProperties::querySdlVideoInfo()
|
||||
return;
|
||||
}
|
||||
|
||||
// Update display related attributes (max FPS, native resolution, etc).
|
||||
// We call the internal variant because we're already in a safe thread context.
|
||||
refreshDisplaysInternal();
|
||||
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
if (!testWindow) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create test window with platform flags: %s",
|
||||
SDL_GetError());
|
||||
|
||||
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||
if (!testWindow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create window for hardware decode test: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
class RefreshDisplaysThread : public QThread
|
||||
{
|
||||
public:
|
||||
RefreshDisplaysThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->refreshDisplaysInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::refreshDisplays()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
RefreshDisplaysThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
refreshDisplaysInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::refreshDisplaysInternal()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
SDL_DisplayMode desktopMode;
|
||||
@@ -120,26 +248,5 @@ void SystemProperties::querySdlVideoInfo()
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
if (!testWindow) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create test window with platform flags: %s",
|
||||
SDL_GetError());
|
||||
|
||||
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||
if (!testWindow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create window for hardware decode test: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class QuerySdlVideoThread;
|
||||
friend class RefreshDisplaysThread;
|
||||
|
||||
public:
|
||||
SystemProperties();
|
||||
|
||||
@@ -15,7 +18,8 @@ public:
|
||||
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
|
||||
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
@@ -23,6 +27,7 @@ public:
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
|
||||
@@ -31,13 +36,16 @@ signals:
|
||||
|
||||
private:
|
||||
void querySdlVideoInfo();
|
||||
void querySdlVideoInfoInternal();
|
||||
void refreshDisplaysInternal();
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
bool isRunningWayland;
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
bool hasWindowManager;
|
||||
QString friendlyNativeArchName;
|
||||
bool hasDesktopEnvironment;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
if (!unknownOptionNames().isEmpty()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
};
|
||||
m_CaptureSysKeysModeMap = {
|
||||
{"never", StreamingPreferences::CSK_OFF},
|
||||
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
@@ -303,6 +308,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
|
||||
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.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());
|
||||
|
||||
@@ -315,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
@@ -405,6 +411,23 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --frame-pacing and --no-frame-pacing options
|
||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||
|
||||
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
|
||||
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
|
||||
|
||||
// Resolve --background-gamepad and --no-background-gamepad options
|
||||
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
|
||||
|
||||
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
|
||||
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
|
||||
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
}
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
|
||||
@@ -53,4 +53,5 @@ private:
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
@@ -85,8 +85,8 @@ public:
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -86,8 +86,8 @@ public:
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
@@ -123,18 +123,18 @@ public:
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>7.1 surround sound audio support</li>
|
||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||
<li>Direct touchscreen mouse control</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||
@@ -35,6 +36,78 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="3.1.2" date="2021-05-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||
<li>Fixed keyboard capture on X11</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.1" date="2021-04-09">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||
<li>Added new community-contributed translations from Weblate</li>
|
||||
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.0" date="2021-03-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||
<li>Added translations for Simplified Chinese and German</li>
|
||||
<li>Added a language selector on the settings page</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance of loading the app grid</li>
|
||||
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||
<li>Fixed window icons on Wayland</li>
|
||||
<li>Fixed number pad enter key functionality in the UI</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.0" date="2021-01-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added the ability to define a custom streaming resolution using the UI</li>
|
||||
<li>Added the option to capture system key combinations (Alt+Tab, Ctrl+Shift+Esc, Win+Tab, etc) in fullscreen mode</li>
|
||||
<li>Updated gamepad mappings are automatically downloaded each time Moonlight launches</li>
|
||||
<li>The performance overlay is now supported for Linux clients using VAAPI and VDPAU hardware acceleration</li>
|
||||
<li>Added the option to enable "Direct Launch" which immediately launches the specified game when clicking on a PC</li>
|
||||
<li>Added the option to mute audio when the Moonlight window loses input focus</li>
|
||||
<li>Added the option to toggle background gamepad input</li>
|
||||
<li>Added the option to invert scroll direction</li>
|
||||
<li>Added the option to swap gamepad button layout to Nintendo-style</li>
|
||||
<li>Added the option to start the Moonlight UI in fullscreen mode</li>
|
||||
<li>The failing TCP or UDP ports are now displayed when the connection fails</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed PS4 controllers not reading inputs during haptics in some games</li>
|
||||
<li>Fixed some slow decoders causing the receive time value to increase rather than the decode time</li>
|
||||
<li>Native resolution options now refresh each time the settings page is opened, rather than once per launch of Moonlight</li>
|
||||
<li>Fixed no video or audio while streaming over Internet connections that drop packets with QoS tagging enabled</li>
|
||||
<li>Improved connection testing accuracy for connections with low MTUs</li>
|
||||
<li>Fixed inability to open an app's context menu with a gamepad</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.2.0" date="2020-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
|
||||
@@ -5,19 +5,9 @@
|
||||
# pairing data will be lost each reboot.
|
||||
HOME=/usr/local/moonlight
|
||||
|
||||
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
|
||||
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
|
||||
# the correct one at runtime.
|
||||
if [ -d "/usr/local/Qt-5.14.1" ]; then
|
||||
BIN="moonlight"
|
||||
else
|
||||
BIN="moonlight59"
|
||||
fi
|
||||
|
||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||
renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
echo Launching $BIN
|
||||
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
|
||||
+166
-94
@@ -10,6 +10,7 @@ CenteredGridView {
|
||||
property AppModel appModel : createModel()
|
||||
property bool activated
|
||||
property bool showHiddenGames
|
||||
property bool showGames
|
||||
|
||||
id: appGrid
|
||||
focus: true
|
||||
@@ -39,6 +40,19 @@ CenteredGridView {
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
if (!showGames && !showHiddenGames) {
|
||||
// Check if there's a direct launch app
|
||||
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
|
||||
if (directLaunchAppIndex >= 0) {
|
||||
// Start the direct launch app if nothing else is running
|
||||
currentIndex = directLaunchAppIndex
|
||||
currentItem.launchOrResumeSelectedApp(false)
|
||||
|
||||
// Set showGames so we will not loop when the stream ends
|
||||
showGames = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
@@ -59,6 +73,9 @@ CenteredGridView {
|
||||
width: 220; height: 287;
|
||||
grid: appGrid
|
||||
|
||||
property alias appContextMenu: appContextMenuLoader.item
|
||||
property alias appNameText: appNameTextLoader.item
|
||||
|
||||
// Dim the app if it's hidden
|
||||
opacity: model.hidden ? 0.4 : 1.0
|
||||
|
||||
@@ -71,9 +88,14 @@ CenteredGridView {
|
||||
source: model.boxart
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
||||
// the image size checks if this is not an app collector game. We know the officially
|
||||
// supported games all have box art, so this check is not required.
|
||||
if (!model.isAppCollectorGame &&
|
||||
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
|
||||
{
|
||||
isPlaceholder = true
|
||||
}
|
||||
@@ -90,97 +112,117 @@ CenteredGridView {
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: resumeButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
active: model.running
|
||||
asynchronous: true
|
||||
anchors.fill: appIcon
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
|
||||
ToolTip.text: "Resume Game"
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: quitButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
id: appNameTextLoader
|
||||
active: appIcon.isPlaceholder
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
// This loader is not asynchronous to avoid noticeable differences
|
||||
// in the time in which the text loads for each game.
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: "Quit Game"
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Label {
|
||||
id: appNameText
|
||||
visible: appIcon.isPlaceholder
|
||||
text: model.name
|
||||
width: appIcon.width
|
||||
height: model.running ? 175 : appIcon.height
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
|
||||
anchors.left: appIcon.left
|
||||
anchors.right: appIcon.right
|
||||
anchors.bottom: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
sourceComponent: Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
font.pointSize: 22
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
{
|
||||
var runningId = appModel.getRunningAppId()
|
||||
if (runningId !== 0 && runningId !== model.appid) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.nextAppName = model.name
|
||||
quitAppDialog.nextAppIndex = index
|
||||
quitAppDialog.open()
|
||||
if (quitExistingApp) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.nextAppName = model.name
|
||||
quitAppDialog.nextAppIndex = index
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": model.name,
|
||||
"session": appModel.createSessionForApp(index),
|
||||
"isResume": runningId === model.appid
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
@@ -190,7 +232,7 @@ CenteredGridView {
|
||||
// will handle starting the game and clicks on the box art will
|
||||
// be ignored.
|
||||
if (!model.running) {
|
||||
launchOrResumeSelectedApp()
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +266,10 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
Keys.onEnterPressed: {
|
||||
// Open the app context menu if activated via the gamepad or keyboard
|
||||
// for running games. If the game isn't running, the above onClicked
|
||||
// method will handle the launch.
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
@@ -232,32 +277,59 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
function doQuitGame() {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: launchOrResumeSelectedApp()
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: "Quit Game"
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: "Hide Game"
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
visible: !model.running || model.hidden
|
||||
Loader {
|
||||
id: appContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +340,7 @@ CenteredGridView {
|
||||
property bool segueToStream : false
|
||||
property string nextAppName: ""
|
||||
property int nextAppIndex: 0
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function quitApp() {
|
||||
|
||||
@@ -19,9 +19,7 @@ ComboBox {
|
||||
id: textMetrics
|
||||
}
|
||||
|
||||
// We call this every time the options change (and init)
|
||||
// so we can adjust the combo box width here too
|
||||
onActivated: {
|
||||
function recalculateWidth() {
|
||||
textMetrics.font = font
|
||||
popupMetrics.font = popup.font
|
||||
textWidth = 0
|
||||
@@ -33,6 +31,10 @@ ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
// We call this every time the options change (and init)
|
||||
// so we can adjust the combo box width here too
|
||||
onActivated: recalculateWidth()
|
||||
|
||||
popup.onAboutToShow: {
|
||||
// Switch to normal navigation for combo boxes
|
||||
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||
|
||||
@@ -6,11 +6,11 @@ import Session 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = "Quitting app..."
|
||||
stageLabel.text = qsTr("Quitting app...")
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
|
||||
@@ -6,11 +6,11 @@ import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onSearchingApp() {
|
||||
stageLabel.text = "Loading app list..."
|
||||
stageLabel.text = qsTr("Loading app list...")
|
||||
}
|
||||
|
||||
function onSessionCreated(appName, session) {
|
||||
@@ -78,7 +78,7 @@ Item {
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
property string appName : ""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
objectName: "Gamepad Mapping"
|
||||
objectName: qsTr("Gamepad Mapping")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Dialog {
|
||||
parent: overlay
|
||||
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
|
||||
parent: ApplicationWindow.contentItem
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
@@ -21,4 +21,7 @@ ItemDelegate {
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
onOpened: {
|
||||
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
||||
// Give focus to the first visible and enabled menu item
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = itemAt(i)
|
||||
if (item.visible && item.enabled) {
|
||||
item.forceActiveFocus(Qt.TabFocusReason)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ MenuItem {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ NavigableDialog {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ ToolButton {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
+73
-63
@@ -17,7 +17,7 @@ CenteredGridView {
|
||||
topMargin: 20
|
||||
bottomMargin: 5
|
||||
cellWidth: 310; cellHeight: 330;
|
||||
objectName: "Computers"
|
||||
objectName: qsTr("Computers")
|
||||
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them.
|
||||
@@ -64,13 +64,13 @@ CenteredGridView {
|
||||
function addComplete(success, detectedPortBlocking)
|
||||
{
|
||||
if (!success) {
|
||||
errorDialog.text = "Unable to connect to the specified PC."
|
||||
errorDialog.text = qsTr("Unable to connect to the specified PC.")
|
||||
|
||||
if (detectedPortBlocking) {
|
||||
errorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
|
||||
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
else {
|
||||
errorDialog.helpText = "Click the Help button for possible solutions."
|
||||
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
|
||||
}
|
||||
|
||||
errorDialog.open()
|
||||
@@ -99,8 +99,8 @@ CenteredGridView {
|
||||
Label {
|
||||
height: searchSpinner.height
|
||||
elide: Label.ElideRight
|
||||
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
|
||||
: "Automatic PC discovery is disabled. Add your PC manually."
|
||||
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
|
||||
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
@@ -113,6 +113,8 @@ CenteredGridView {
|
||||
width: 300; height: 320;
|
||||
grid: pcGrid
|
||||
|
||||
property alias pcContextMenu : pcContextMenuLoader.item
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -160,66 +162,70 @@ CenteredGridView {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "View Apps"
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
stackView.push(appView)
|
||||
Loader {
|
||||
id: pcContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "View Hidden Apps"
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Test Network"
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Rename PC"
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Delete PC"
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.online) {
|
||||
if (model.paired) {
|
||||
if (!model.serverSupported) {
|
||||
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
else if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
@@ -238,7 +244,7 @@ CenteredGridView {
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = "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.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()
|
||||
}
|
||||
@@ -293,7 +299,7 @@ CenteredGridView {
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
property string pin : "0000"
|
||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
|
||||
standardButtons: Dialog.Cancel
|
||||
onRejected: {
|
||||
// FIXME: We should interrupt pairing here
|
||||
@@ -304,7 +310,7 @@ CenteredGridView {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
property int pcIndex : -1;
|
||||
text:"Are you sure you want to remove this PC?"
|
||||
text:qsTr("Are you sure you want to remove this PC?")
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function deletePc() {
|
||||
@@ -321,22 +327,22 @@ CenteredGridView {
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
onAboutToShow: {
|
||||
testConnectionDialog.text = "Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.\n\nThis may take a few seconds…"
|
||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||
showSpinner = true
|
||||
}
|
||||
|
||||
function connectionTestComplete(result, blockedPorts)
|
||||
{
|
||||
if (result === -1) {
|
||||
text = "The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later."
|
||||
text = qsTr("The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.")
|
||||
imageSrc = "qrc:/res/baseline-warning-24px.svg"
|
||||
}
|
||||
else if (result === 0) {
|
||||
text = "This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nIf you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection."
|
||||
text = qsTr("This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.") + "\n\n" + qsTr("If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.")
|
||||
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
|
||||
}
|
||||
else {
|
||||
text = "Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.\n\nThe following network ports were blocked:\n"
|
||||
text = qsTr("Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.") + "\n\n" + qsTr("The following network ports were blocked:") + "\n"
|
||||
text += blockedPorts
|
||||
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
|
||||
}
|
||||
@@ -348,7 +354,7 @@ CenteredGridView {
|
||||
|
||||
NavigableDialog {
|
||||
id: renamePcDialog
|
||||
property string label: "Enter the new name for this PC:"
|
||||
property string label: qsTr("Enter the new name for this PC:")
|
||||
property string originalName
|
||||
property int pcIndex : -1;
|
||||
|
||||
@@ -384,6 +390,10 @@ CenteredGridView {
|
||||
Keys.onReturnPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ Item {
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
property string stageText : "Quitting " + appName + "..."
|
||||
property string stageText : qsTr("Quitting %1...").arg(appName)
|
||||
|
||||
function quitAppCompleted(error)
|
||||
{
|
||||
|
||||
+600
-132
File diff suppressed because it is too large
Load Diff
+23
-8
@@ -8,19 +8,25 @@ import Session 1.0
|
||||
Item {
|
||||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
|
||||
qsTr("Starting %1...").arg(appName)
|
||||
property bool isResume : false
|
||||
property bool quitAfter : false
|
||||
|
||||
function stageStarting(stage)
|
||||
{
|
||||
// Update the spinner text
|
||||
stageText = "Starting " + stage + "..."
|
||||
stageText = qsTr("Starting %1...").arg(stage)
|
||||
}
|
||||
|
||||
function stageFailed(stage, errorCode)
|
||||
function stageFailed(stage, errorCode, failingPorts)
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
|
||||
|
||||
if (failingPorts) {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
|
||||
}
|
||||
}
|
||||
|
||||
function connectionStarted()
|
||||
@@ -65,7 +71,7 @@ Item {
|
||||
function sessionFinished(portTestResult)
|
||||
{
|
||||
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
@@ -97,6 +103,11 @@ Item {
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Garbage collect the Session object since it's pretty heavyweight
|
||||
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||
session = null
|
||||
gc()
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
@@ -147,13 +158,17 @@ Item {
|
||||
// in the hintText control itself to synchronize
|
||||
// with Session.exec() which requires no concurrent
|
||||
// gamepad usage.
|
||||
hintText.text = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
||||
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
|
||||
|
||||
// Stop GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.disable()
|
||||
|
||||
// Garbage collect QML stuff before we start streaming,
|
||||
// since we'll probably be streaming for a while and we
|
||||
// won't be able to GC during the stream.
|
||||
gc()
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,17 @@ Session* AppModel::createSessionForApp(int appIndex)
|
||||
return new Session(m_Computer, app);
|
||||
}
|
||||
|
||||
int AppModel::getDirectLaunchAppIndex()
|
||||
{
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
if (m_VisibleApps[i].directLaunch) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int AppModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// For list models only the root node (an invalid parent) should return the list's size. For all
|
||||
@@ -78,6 +89,10 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
return app.hidden;
|
||||
case AppIdRole:
|
||||
return app.id;
|
||||
case DirectLaunchRole:
|
||||
return app.directLaunch;
|
||||
case AppCollectorGameRole:
|
||||
return app.isAppCollectorGame;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -92,6 +107,8 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
||||
names[BoxArtRole] = "boxart";
|
||||
names[HiddenRole] = "hidden";
|
||||
names[AppIdRole] = "appid";
|
||||
names[DirectLaunchRole] = "directLaunch";
|
||||
names[AppCollectorGameRole] = "appCollectorGame";
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -207,6 +224,32 @@ void AppModel::setAppHidden(int appIndex, bool hidden)
|
||||
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||
}
|
||||
|
||||
void AppModel::setAppDirectLaunch(int appIndex, bool directLaunch)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||
int appId = m_VisibleApps.at(appIndex).id;
|
||||
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
|
||||
for (NvApp& app : m_Computer->appList) {
|
||||
if (directLaunch) {
|
||||
// We must clear direct launch from all other apps
|
||||
// to set it on the new app.
|
||||
app.directLaunch = app.id == appId;
|
||||
}
|
||||
else if (app.id == appId) {
|
||||
// If we're clearing direct launch, we're done once we
|
||||
// find our matching app ID.
|
||||
app.directLaunch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||
}
|
||||
|
||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// Ignore updates for computers that aren't ours
|
||||
|
||||
@@ -17,6 +17,8 @@ class AppModel : public QAbstractListModel
|
||||
BoxArtRole,
|
||||
HiddenRole,
|
||||
AppIdRole,
|
||||
DirectLaunchRole,
|
||||
AppCollectorGameRole,
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -27,6 +29,8 @@ public:
|
||||
|
||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||
|
||||
Q_INVOKABLE int getDirectLaunchAppIndex();
|
||||
|
||||
Q_INVOKABLE int getRunningAppId();
|
||||
|
||||
Q_INVOKABLE QString getRunningAppName();
|
||||
@@ -35,6 +39,8 @@ public:
|
||||
|
||||
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
|
||||
|
||||
Q_INVOKABLE void setAppDirectLaunch(int appIndex, bool directLaunch);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
@@ -42,6 +42,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
return !computer->macAddress.isEmpty();
|
||||
case StatusUnknownRole:
|
||||
return computer->state == NvComputer::CS_UNKNOWN;
|
||||
case ServerSupportedRole:
|
||||
return computer->isSupportedServerVersion;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -68,6 +70,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
names[BusyRole] = "busy";
|
||||
names[WakeableRole] = "wakeable";
|
||||
names[StatusUnknownRole] = "statusUnknown";
|
||||
names[ServerSupportedRole] = "serverSupported";
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -158,26 +161,9 @@ public:
|
||||
emit connectionTestCompleted(-1, QString());
|
||||
}
|
||||
else {
|
||||
QString blockedPorts;
|
||||
|
||||
for (int i = 0; i < 32; i++) {
|
||||
if (portTestResult & (1 << i)) {
|
||||
if (!blockedPorts.isEmpty()) {
|
||||
blockedPorts += "\n";
|
||||
}
|
||||
|
||||
if (LiGetProtocolFromPortFlagIndex(i) == 17 /* IPPROTO_UDP */) {
|
||||
blockedPorts += "UDP ";
|
||||
}
|
||||
else {
|
||||
blockedPorts += "TCP ";
|
||||
}
|
||||
|
||||
blockedPorts += QString::number(LiGetPortFromPortFlagIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
emit connectionTestCompleted(portTestResult, blockedPorts);
|
||||
char blockedPorts[512];
|
||||
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
|
||||
emit connectionTestCompleted(portTestResult, QString(blockedPorts));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
|
||||
PairedRole,
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
StatusUnknownRole
|
||||
StatusUnknownRole,
|
||||
ServerSupportedRole
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
+60
-28
@@ -12,12 +12,40 @@ import SdlGamepadKeyNavigation 1.0
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
|
||||
// Set by SettingsView to force the back operation to pop all
|
||||
// pages except the initial view. This is required when doing
|
||||
// a retranslate() because AppView breaks for some reason.
|
||||
property bool clearOnBack: false
|
||||
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
visibility: (SystemProperties.hasWindowManager && StreamingPreferences.startWindowed) ? "Windowed" : "Maximized"
|
||||
visibility: {
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
||||
} else {
|
||||
return "FullScreen"
|
||||
}
|
||||
}
|
||||
|
||||
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
|
||||
// it will never insert a line break and just extend on forever.
|
||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
// Pop all items except the first one
|
||||
stackView.pop(null)
|
||||
clearOnBack = false
|
||||
}
|
||||
else {
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
@@ -34,7 +62,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -43,7 +71,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onBackPressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -198,7 +226,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/arrow_left.svg"
|
||||
|
||||
onClicked: stackView.pop()
|
||||
onClicked: goBack()
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -223,8 +251,8 @@ ApplicationWindow {
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: stackView.currentItem.objectName === "Settings"
|
||||
text: "Version " + SystemProperties.versionString
|
||||
visible: stackView.currentItem.objectName === qsTr("Settings")
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
@@ -233,14 +261,14 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
stackView.currentItem.objectName === "Settings"
|
||||
stackView.currentItem.objectName === qsTr("Settings")
|
||||
|
||||
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Join our community on Discord"
|
||||
ToolTip.text: qsTr("Join our community on Discord")
|
||||
|
||||
// TODO need to make sure browser is brought to foreground.
|
||||
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
|
||||
@@ -252,14 +280,14 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: stackView.currentItem.objectName === "Computers"
|
||||
visible: stackView.currentItem.objectName === qsTr("Computers")
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: newPcShortcut
|
||||
@@ -299,7 +327,7 @@ ApplicationWindow {
|
||||
|
||||
function updateAvailable(version, url)
|
||||
{
|
||||
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||
updateButton.browserUrl = url
|
||||
updateButton.visible = true
|
||||
}
|
||||
@@ -323,7 +351,7 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: helpShortcut
|
||||
@@ -346,11 +374,11 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Gamepad Mapper"
|
||||
ToolTip.text: qsTr("Gamepad Mapper")
|
||||
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -362,7 +390,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -377,31 +405,31 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration."
|
||||
helpText: "Click the Help button for more information on solving this problem."
|
||||
text: qsTr("No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration.")
|
||||
helpText: qsTr("Click the Help button for more information on solving this problem.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: xWaylandDialog
|
||||
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||
"Try running with QT_QPA_PLATFORM=wayland or switch to X11."
|
||||
helpText: "Click the Help button for more information."
|
||||
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
|
||||
helpText: qsTr("Click the Help button for more information.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: wow64Dialog
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||
text: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
|
||||
onAccepted: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||
}
|
||||
@@ -410,9 +438,9 @@ ApplicationWindow {
|
||||
ErrorMessageDialog {
|
||||
id: unmappedGamepadDialog
|
||||
property string unmappedGamepads : ""
|
||||
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
|
||||
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
|
||||
helpTextSeparator: "\n\n"
|
||||
helpText: "Click the Help button for information on how to map your gamepads."
|
||||
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
||||
}
|
||||
|
||||
@@ -420,7 +448,7 @@ ApplicationWindow {
|
||||
NavigableMessageDialog {
|
||||
id: quitConfirmationDialog
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
text: "Are you sure you want to quit?"
|
||||
text: qsTr("Are you sure you want to quit?")
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: Qt.quit()
|
||||
}
|
||||
@@ -450,7 +478,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableDialog {
|
||||
id: addPcDialog
|
||||
property string label: "Enter the IP address of your GameStream PC:"
|
||||
property string label: qsTr("Enter the IP address of your GameStream PC:")
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
@@ -465,7 +493,7 @@ ApplicationWindow {
|
||||
|
||||
onAccepted: {
|
||||
if (editText.text) {
|
||||
ComputerManager.addNewHost(editText.text, false)
|
||||
ComputerManager.addNewHost(editText.text.trim(), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +511,10 @@ ApplicationWindow {
|
||||
Keys.onReturnPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||
@@ -14,7 +15,7 @@ SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
@@ -88,6 +89,7 @@ void SdlGamepadKeyNavigation::disable()
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
StreamingPreferences prefs;
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@@ -104,6 +106,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
||||
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||
|
||||
// Swap face buttons if needed
|
||||
if (prefs.swapFaceButtons) {
|
||||
switch (event.cbutton.button) {
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.cbutton.button) {
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||
if (m_UiNavMode) {
|
||||
|
||||
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.
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
+59
-9
@@ -22,7 +22,7 @@
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32)
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#include "antihookingprotection.h"
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include <openssl/ssl.h>
|
||||
@@ -196,7 +196,7 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
WCHAR dmpFileName[MAX_PATH];
|
||||
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||
QString qDmpFileName = QString::fromUtf16((const char16_t*)dmpFileName);
|
||||
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||
@@ -288,9 +288,10 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32)
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll by calling a dummy function.
|
||||
// by ntdll on x86/x64 platforms by calling a dummy function.
|
||||
// AntiHooking.dll is not currently built for ARM64.
|
||||
AntiHookingDummyImport();
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// Force libssl.so to be directly linked to our binary, so
|
||||
@@ -305,8 +306,10 @@ int main(int argc, char *argv[])
|
||||
// NB: We can't use QGuiApplication::platformName() here because it is only
|
||||
// set once the QGuiApplication is created, which is too late to enable High DPI :(
|
||||
if (WMUtils::isRunningWindowManager()) {
|
||||
// Enable High DPI support
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
// Enable High DPI support on Qt 5.x. It is always enabled on Qt 6.0
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
// Enable fractional High DPI scaling on Qt 5.14 and later
|
||||
@@ -319,6 +322,13 @@ int main(int argc, char *argv[])
|
||||
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
|
||||
qputenv("QT_QPA_PLATFORM", "eglfs");
|
||||
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_ALWAYS_SET_MODE")) {
|
||||
qInfo() << "Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.";
|
||||
|
||||
// The UI doesn't appear on RetroPie without this option.
|
||||
qputenv("QT_QPA_EGLFS_ALWAYS_SET_MODE", "1");
|
||||
}
|
||||
|
||||
if (!QFile("/dev/dri").exists()) {
|
||||
qWarning() << "Unable to find a KMSDRM display device!";
|
||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||
@@ -331,7 +341,7 @@ int main(int argc, char *argv[])
|
||||
// password prompts on macOS.
|
||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||
@@ -340,6 +350,18 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Moonlight requires the non-threaded renderer because we depend
|
||||
// on being able to control the render thread by blocking in the
|
||||
// main thread (and pumping events from the main thread when needed).
|
||||
// That doesn't work with the threaded renderer which causes all
|
||||
// sorts of odd behavior depending on the platform.
|
||||
//
|
||||
// NB: Windows defaults to the "windows" non-threaded render loop on
|
||||
// Qt 5 and the threaded render loop on Qt 6.
|
||||
qputenv("QSG_RENDER_LOOP", "basic");
|
||||
#endif
|
||||
|
||||
// We don't want system proxies to apply to us
|
||||
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||
|
||||
@@ -373,6 +395,14 @@ int main(int argc, char *argv[])
|
||||
// future KMSDRM platforms that use SDL for rendering.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
||||
|
||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||
|
||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
||||
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||
SDL_SetHint("SDL_WINDOWS_USE_D3D9EX", "1");
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
|
||||
@@ -408,6 +438,14 @@ int main(int argc, char *argv[])
|
||||
// releases of Moonlight.
|
||||
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
|
||||
|
||||
// Disable relative mouse scaling to renderer size or logical DPI. We want to send
|
||||
// the mouse motion exactly how it was given to us.
|
||||
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
|
||||
|
||||
// Set our app name for SDL to use with PulseAudio. This matches what we provide
|
||||
// as our app name to libsoundio too.
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
|
||||
#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
|
||||
@@ -417,6 +455,10 @@ int main(int argc, char *argv[])
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
@@ -455,7 +497,16 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef Q_OS_DARWIN
|
||||
// Set the window icon except on macOS where we want to keep the
|
||||
// modified macOS 11 style rounded corner icon.
|
||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
// Register our C++ types for QML
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
@@ -483,8 +534,8 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||
"StreamingPreferences",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences();
|
||||
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences(qmlEngine);
|
||||
});
|
||||
|
||||
// Create the identity manager on the main thread
|
||||
@@ -551,7 +602,6 @@ int main(int argc, char *argv[])
|
||||
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
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file contains hooks for several functions that allow
|
||||
// Qt and SDL to more or less share DRM master ownership.
|
||||
//
|
||||
// This technique requires Linux v5.8 or later, or for Moonlight
|
||||
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
|
||||
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
|
||||
// our trick from working (without root, that is).
|
||||
//
|
||||
// The specific kernel change required to run without root is:
|
||||
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// We require SDL 2.0.15+ to hook because it supports sharing
|
||||
// the DRM FD with our code. This avoids having multiple DRM FDs
|
||||
// in flight at the same time which would significantly complicate
|
||||
// the logic here because we'd need to figure out exactly which FD
|
||||
// should be the master at any given time. With the position of our
|
||||
// hooks, that is definitely not trivial.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
// This hook will handle legacy DRM rendering
|
||||
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
}
|
||||
|
||||
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||
// hook this variant of open(), since that's what SDL uses. When we see
|
||||
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||
// master on the Qt FD to allow the new FD to have master.
|
||||
int open(const char *pathname, int flags)
|
||||
{
|
||||
// Call the real thing to do the open operation
|
||||
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// It's a DRM device, but is it _our_ DRM device?
|
||||
struct stat64 fdstat;
|
||||
|
||||
fstat64(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Our close() hook handles restoring DRM master to the Qt FD
|
||||
// after SDL closes its DRM FD.
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
if (ret == 0) {
|
||||
// If we just closed the SDL DRM master FD, restore master
|
||||
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||
// was master once before, so we can set it as master again
|
||||
// using drmSetMaster() without CAP_SYS_ADMIN.
|
||||
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
|
||||
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
g_SdlDrmMasterFd = -1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
Binary file not shown.
+40
-2
@@ -6,6 +6,7 @@
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
|
||||
@@ -28,11 +29,43 @@ QByteArray Path::readDataFile(QString fileName)
|
||||
return dataFile.readAll();
|
||||
}
|
||||
|
||||
void Path::writeCacheFile(QString fileName, QByteArray data)
|
||||
{
|
||||
QDir cacheDir(s_CacheDir);
|
||||
|
||||
// Create the cache path if it does not exist
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkpath(".");
|
||||
}
|
||||
|
||||
QFile dataFile(cacheDir.absoluteFilePath(fileName));
|
||||
dataFile.open(QIODevice::WriteOnly);
|
||||
dataFile.write(data);
|
||||
}
|
||||
|
||||
void Path::deleteCacheFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(QDir(s_CacheDir).absoluteFilePath(fileName));
|
||||
dataFile.remove();
|
||||
}
|
||||
|
||||
QFileInfo Path::getCacheFileInfo(QString fileName)
|
||||
{
|
||||
return QFileInfo(QDir(s_CacheDir), fileName);
|
||||
}
|
||||
|
||||
QString Path::getDataFilePath(QString fileName)
|
||||
{
|
||||
QString candidatePath;
|
||||
|
||||
// Check the current directory first
|
||||
// Check the cache location first (used by Path::writeDataFile())
|
||||
candidatePath = QDir(s_CacheDir).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Check the current directory
|
||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
@@ -40,7 +73,7 @@ QString Path::getDataFilePath(QString fileName)
|
||||
}
|
||||
|
||||
// Now check the data directories (for Linux, in particular)
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName);
|
||||
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
@@ -64,6 +97,10 @@ void Path::initialize(bool portable)
|
||||
if (portable) {
|
||||
s_LogDir = QDir::currentPath();
|
||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||
|
||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||
// the cache directory must be different than the current directory.
|
||||
s_CacheDir = QDir::currentPath() + "/cache";
|
||||
}
|
||||
else {
|
||||
#ifdef Q_OS_DARWIN
|
||||
@@ -73,6 +110,7 @@ void Path::initialize(bool portable)
|
||||
#else
|
||||
s_LogDir = QDir::tempPath();
|
||||
#endif
|
||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QFileInfo>
|
||||
|
||||
class Path
|
||||
{
|
||||
@@ -10,6 +11,9 @@ public:
|
||||
static QString getBoxArtCacheDir();
|
||||
|
||||
static QByteArray readDataFile(QString fileName);
|
||||
static void writeCacheFile(QString fileName, QByteArray data);
|
||||
static void deleteCacheFile(QString fileName);
|
||||
static QFileInfo getCacheFileInfo(QString fileName);
|
||||
|
||||
// Only safe to use directly for Qt classes
|
||||
static QString getDataFilePath(QString fileName);
|
||||
@@ -17,6 +21,7 @@ public:
|
||||
static void initialize(bool portable);
|
||||
|
||||
private:
|
||||
static QString s_CacheDir;
|
||||
static QString s_LogDir;
|
||||
static QString s_BoxArtCacheDir;
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240" fill="white"><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+18
-2
@@ -17,11 +17,27 @@
|
||||
<file>res/baseline-error_outline-24px.svg</file>
|
||||
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||
<file>res/Discord-Logo-White.svg</file>
|
||||
<file>languages/qml_de.ts</file>
|
||||
<file>languages/qml_de.qm</file>
|
||||
<file>languages/qml_fr.ts</file>
|
||||
<file>languages/qml_fr.qm</file>
|
||||
<file>languages/qml_zh_cn.ts</file>
|
||||
<file>languages/qml_zh_cn.qm</file>
|
||||
<file>languages/qml_nb_NO.ts</file>
|
||||
<file>languages/qml_nb_NO.qm</file>
|
||||
<file>languages/qml_ru.ts</file>
|
||||
<file>languages/qml_ru.qm</file>
|
||||
<file>languages/qml_es.ts</file>
|
||||
<file>languages/qml_es.qm</file>
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||
<file alias="egl.frag">shaders/egl.frag</file>
|
||||
<file alias="egl.vert">shaders/egl.vert</file>
|
||||
<file alias="egl_nv12.frag">shaders/egl_nv12.frag</file>
|
||||
<file alias="egl_nv12.vert">shaders/egl_nv12.vert</file>
|
||||
<file alias="egl_opaque.frag">shaders/egl_opaque.frag</file>
|
||||
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
|
||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "compatfetcher.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
|
||||
#define COMPAT_VERSION "v1"
|
||||
#define COMPAT_KEY "latestsupportedversion-"
|
||||
|
||||
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
{
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &CompatFetcher::handleCompatInfoFetched);
|
||||
}
|
||||
|
||||
void CompatFetcher::start()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QUrl url("https://moonlight-stream.org/compatibility/" COMPAT_VERSION);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
}
|
||||
|
||||
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
if (gfeVersion.isEmpty()) {
|
||||
// If we don't have a GFE version, just allow it
|
||||
return true;
|
||||
}
|
||||
|
||||
QString latestSupportedVersion = settings.value(COMPAT_KEY COMPAT_VERSION).toString();
|
||||
if (latestSupportedVersion.isEmpty()) {
|
||||
// We don't have compat data yet, so just assume it's supported
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList latestSupportedVersionQuad = latestSupportedVersion.split('.');
|
||||
QStringList gfeVersionQuad = gfeVersion.split('.');
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int actualVerVal = 0;
|
||||
int latestSupportedVal = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < gfeVersionQuad.count()) {
|
||||
actualVerVal = gfeVersionQuad[i].toInt();
|
||||
}
|
||||
if (i < latestSupportedVersionQuad.count()) {
|
||||
latestSupportedVal = latestSupportedVersionQuad[i].toInt();
|
||||
}
|
||||
|
||||
if (i >= gfeVersionQuad.count() && i >= latestSupportedVersionQuad.count()) {
|
||||
// Equal versions - this is fine
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actualVerVal < latestSupportedVal) {
|
||||
// Actual version is lower than latest supported - this is fine
|
||||
return true;
|
||||
}
|
||||
else if (actualVerVal > latestSupportedVal) {
|
||||
// Actual version is greater than latest supported - this is bad
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
QString version = QString(reply->readAll()).trimmed();
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue(COMPAT_KEY COMPAT_VERSION, version);
|
||||
|
||||
qInfo() << "Latest supported GFE server:" << version;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Failed to download latest compatibility data:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class CompatFetcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CompatFetcher(QObject *parent = nullptr);
|
||||
|
||||
void start();
|
||||
|
||||
static bool isGfeVersionSupported(QString gfeVersion);
|
||||
|
||||
private slots:
|
||||
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "mappingfetcher.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
|
||||
MappingFetcher::MappingFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
{
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &MappingFetcher::handleMappingListFetched);
|
||||
}
|
||||
|
||||
void MappingFetcher::start()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QUrl url("https://moonlight-stream.org/SDL_GameControllerDB/gamecontrollerdb.txt");
|
||||
QNetworkRequest request(url);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
|
||||
// Qt 5.12 introduced QNetworkRequest::IfModifiedSinceHeader. We _could_ implement it
|
||||
// by hand (including QDateTime conversion to the correct string form) for earlier Qt
|
||||
// versions, but that's a pain and this is just an optimization anyway. We'll do a bit
|
||||
// of extra work on those legacy Qt versions by fetching the GCDB each time we launch.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||
// Only download the file if it's newer than what we have
|
||||
QFileInfo existingFileInfo = Path::getCacheFileInfo("gamecontrollerdb.txt");
|
||||
if (existingFileInfo.exists()) {
|
||||
// Make sure the cached file looks reasonable. It should have some data and
|
||||
// the last modified time should not be in the future.
|
||||
QDateTime lastModifiedTime = existingFileInfo.lastModified().toUTC();
|
||||
if (existingFileInfo.size() > 0 && lastModifiedTime <= QDateTime::currentDateTimeUtc()) {
|
||||
request.setHeader(QNetworkRequest::IfModifiedSinceHeader, existingFileInfo.lastModified().toUTC());
|
||||
}
|
||||
else {
|
||||
Path::deleteCacheFile("gamecontrollerdb.txt");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
}
|
||||
|
||||
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
// If we get a 304 back, Qt will happily just tell us our request was
|
||||
// successful and let us try to write our empty response to disk. Check
|
||||
// the status code directly to prevent this.
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 304) {
|
||||
qInfo() << "Gamepad mappings are up to date";
|
||||
}
|
||||
else {
|
||||
// Update the cached data on disk for next call to applyMappings()
|
||||
QByteArray data = reply->readAll();
|
||||
if (!data.isEmpty()) {
|
||||
Path::writeCacheFile("gamecontrollerdb.txt", data);
|
||||
}
|
||||
|
||||
qInfo() << "Downloaded updated gamepad mappings";
|
||||
}
|
||||
}
|
||||
else {
|
||||
qWarning() << "Failed to download updated gamepad mappings:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class MappingFetcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MappingFetcher(QObject *parent = nullptr);
|
||||
|
||||
void start();
|
||||
|
||||
private slots:
|
||||
void handleMappingListFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -10,10 +10,18 @@
|
||||
#define SER_GUID "guid"
|
||||
#define SER_MAPPING "mapping"
|
||||
|
||||
MappingFetcher* MappingManager::s_MappingFetcher;
|
||||
|
||||
MappingManager::MappingManager()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// Load updated mappings from the Internet once per Moonlight launch
|
||||
if (s_MappingFetcher == nullptr) {
|
||||
s_MappingFetcher = new MappingFetcher();
|
||||
s_MappingFetcher->start();
|
||||
}
|
||||
|
||||
// First load existing saved mappings. This ensures the user's
|
||||
// hints can always override the old data.
|
||||
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
|
||||
@@ -33,7 +41,7 @@ MappingManager::MappingManager()
|
||||
#else
|
||||
.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
for (QString sdlMapping : sdlMappings) {
|
||||
for (const QString& sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
}
|
||||
@@ -64,16 +72,26 @@ void MappingManager::applyMappings()
|
||||
if (!mappingData.isEmpty()) {
|
||||
int newMappings = SDL_GameControllerAddMappingsFromRW(
|
||||
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
|
||||
if (newMappings < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading gamepad mappings: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
else {
|
||||
|
||||
if (newMappings > 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loaded %d new gamepad mappings",
|
||||
newMappings);
|
||||
}
|
||||
else {
|
||||
if (newMappings < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading gamepad mappings: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
else if (newMappings == 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"0 new mappings found in gamecontrollerdb.txt. Is it corrupt?");
|
||||
}
|
||||
|
||||
// Try deleting the cached mapping list just in case it's corrupt
|
||||
Path::deleteCacheFile("gamecontrollerdb.txt");
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "mappingfetcher.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
class SdlGamepadMapping
|
||||
@@ -70,5 +72,7 @@ public:
|
||||
|
||||
private:
|
||||
QMap<QString, SdlGamepadMapping> m_Mappings;
|
||||
|
||||
static MappingFetcher* s_MappingFetcher;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "streamingpreferences.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
#include <QLocale>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#define SER_STREAMSETTINGS "streamsettings"
|
||||
#define SER_WIDTH "width"
|
||||
@@ -24,17 +29,32 @@
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
#define SER_FRAMEPACING "framepacing"
|
||||
#define SER_CONNWARNINGS "connwarnings"
|
||||
#define SER_UIDISPLAYMODE "uidisplaymode"
|
||||
#define SER_RICHPRESENCE "richpresence"
|
||||
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||
#define SER_DEFAULTVER "defaultver"
|
||||
#define SER_PACKETSIZE "packetsize"
|
||||
#define SER_DETECTNETBLOCKING "detectnetblocking"
|
||||
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
|
||||
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
|
||||
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
#define SER_LANGUAGE "language"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(nullptr)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(qmlEngine)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
@@ -64,7 +84,6 @@ void StreamingPreferences::reload()
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
|
||||
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
|
||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||
@@ -72,6 +91,12 @@ void StreamingPreferences::reload()
|
||||
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
|
||||
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
|
||||
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
|
||||
muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -82,6 +107,12 @@ void StreamingPreferences::reload()
|
||||
// Try to load from the old preference value too
|
||||
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
|
||||
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
||||
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
||||
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
||||
language = static_cast<Language>(settings.value(SER_LANGUAGE,
|
||||
static_cast<int>(Language::LANG_AUTO)).toInt());
|
||||
|
||||
|
||||
// Perform default settings updates as required based on last default version
|
||||
if (defaultVer == 0) {
|
||||
@@ -94,6 +125,82 @@ void StreamingPreferences::reload()
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamingPreferences::retranslate()
|
||||
{
|
||||
static QTranslator* translator = nullptr;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
if (m_QmlEngine != nullptr) {
|
||||
// Dynamic retranslation is not supported until Qt 5.10
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
QTranslator* newTranslator = new QTranslator();
|
||||
QString languageSuffix = getSuffixFromLanguage(language);
|
||||
|
||||
// Remove the old translator, even if we can't load a new one.
|
||||
// Otherwise we'll be stuck with the old translated values instead
|
||||
// of defaulting to English.
|
||||
if (translator != nullptr) {
|
||||
QCoreApplication::removeTranslator(translator);
|
||||
delete translator;
|
||||
translator = nullptr;
|
||||
}
|
||||
|
||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||
|
||||
translator = newTranslator;
|
||||
QCoreApplication::installTranslator(translator);
|
||||
}
|
||||
else {
|
||||
qInfo() << "No translation available for " << languageSuffix;
|
||||
delete newTranslator;
|
||||
}
|
||||
|
||||
if (m_QmlEngine != nullptr) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
// This is a dynamic retranslation from the settings page.
|
||||
// We have to kick the QML engine into reloading our text.
|
||||
m_QmlEngine->retranslate();
|
||||
#else
|
||||
// Unreachable below Qt 5.10 due to the check above
|
||||
Q_ASSERT(false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// This is a translation from a non-QML context, which means
|
||||
// it is probably app startup. There's nothing to refresh.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
|
||||
{
|
||||
switch (lang)
|
||||
{
|
||||
case LANG_DE:
|
||||
return "de";
|
||||
case LANG_EN:
|
||||
return "en";
|
||||
case LANG_FR:
|
||||
return "fr";
|
||||
case LANG_ZH_CN:
|
||||
return "zh_cn";
|
||||
case LANG_NB_NO:
|
||||
return "nb_NO";
|
||||
case LANG_RU:
|
||||
return "ru";
|
||||
case LANG_ES:
|
||||
return "es";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
{
|
||||
QSettings settings;
|
||||
@@ -111,7 +218,6 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
|
||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||
@@ -122,8 +228,15 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
||||
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
|
||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
||||
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
@@ -133,8 +246,14 @@ int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return static_cast<int>(1000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 854 * 480) {
|
||||
return static_cast<int>(1500 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
if (width * height <= 1366 * 768) {
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return static_cast<int>(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class StreamingPreferences : public QObject
|
||||
{
|
||||
@@ -9,6 +10,7 @@ class StreamingPreferences : public QObject
|
||||
|
||||
public:
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
@@ -50,6 +52,38 @@ public:
|
||||
};
|
||||
Q_ENUM(WindowMode)
|
||||
|
||||
enum UIDisplayMode
|
||||
{
|
||||
UI_WINDOWED,
|
||||
UI_MAXIMIZED,
|
||||
UI_FULLSCREEN
|
||||
};
|
||||
Q_ENUM(UIDisplayMode)
|
||||
|
||||
// New entries must go at the end of the enum
|
||||
// to avoid renumbering existing entries (which
|
||||
// would affect existing user preferences).
|
||||
enum Language
|
||||
{
|
||||
LANG_AUTO,
|
||||
LANG_EN,
|
||||
LANG_FR,
|
||||
LANG_ZH_CN,
|
||||
LANG_DE,
|
||||
LANG_NB_NO,
|
||||
LANG_RU,
|
||||
LANG_ES
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
enum CaptureSysKeysMode
|
||||
{
|
||||
CSK_OFF,
|
||||
CSK_FULLSCREEN,
|
||||
CSK_ALWAYS,
|
||||
};
|
||||
Q_ENUM(CaptureSysKeysMode);
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
@@ -63,7 +97,6 @@ public:
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
|
||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||
@@ -74,7 +107,16 @@ public:
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||
Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged)
|
||||
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
|
||||
Q_PROPERTY(bool muteOnFocusLoss MEMBER muteOnFocusLoss NOTIFY muteOnFocusLossChanged)
|
||||
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(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||
|
||||
Q_INVOKABLE bool retranslate();
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -90,19 +132,25 @@ public:
|
||||
bool quitAppAfter;
|
||||
bool absoluteMouseMode;
|
||||
bool absoluteTouchMode;
|
||||
bool startWindowed;
|
||||
bool framePacing;
|
||||
bool connectionWarnings;
|
||||
bool richPresence;
|
||||
bool gamepadMouse;
|
||||
bool detectNetworkBlocking;
|
||||
bool swapMouseButtons;
|
||||
bool muteOnFocusLoss;
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
Language language;
|
||||
CaptureSysKeysMode captureSysKeysMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -119,13 +167,24 @@ signals:
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void uiDisplayModeChanged();
|
||||
void windowModeChanged();
|
||||
void startWindowedChanged();
|
||||
void framePacingChanged();
|
||||
void connectionWarningsChanged();
|
||||
void richPresenceChanged();
|
||||
void gamepadMouseChanged();
|
||||
void detectNetworkBlockingChanged();
|
||||
void mouseButtonsChanged();
|
||||
void muteOnFocusLossChanged();
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysModeChanged();
|
||||
void languageChanged();
|
||||
|
||||
private:
|
||||
QString getSuffixFromLanguage(Language lang);
|
||||
|
||||
QQmlEngine* m_QmlEngine;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 vTextCoord;
|
||||
|
||||
uniform samplerExternalOES uTexture;
|
||||
|
||||
void main() {
|
||||
FragColor = texture2D(uTexture, vTextCoord);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 300 es
|
||||
|
||||
layout (location = 0) in vec2 aPosition; // 2D: X,Y
|
||||
layout (location = 1) in vec2 aTexCoord;
|
||||
out vec2 vTextCoord;
|
||||
|
||||
void main() {
|
||||
vTextCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
precision mediump float;
|
||||
uniform sampler2D uTexture;
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vec4 abgr = texture2D(uTexture, vTexCoord);
|
||||
|
||||
gl_FragColor = abgr;
|
||||
gl_FragColor.r = abgr.b;
|
||||
gl_FragColor.b = abgr.r;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
attribute vec2 aPosition; // 2D: X,Y
|
||||
attribute vec2 aTexCoord;
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vTexCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -197,6 +197,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
|
||||
s_ActiveSession->m_AudioSampleCount++;
|
||||
|
||||
// If audio is muted, don't decode or play the audio
|
||||
if (s_ActiveSession->m_AudioMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
|
||||
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#include "qtaud.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
QtAudioRenderer::QtAudioRenderer()
|
||||
: m_AudioOutput(nullptr),
|
||||
m_OutputDevice(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QtAudioRenderer::~QtAudioRenderer()
|
||||
{
|
||||
delete m_AudioOutput;
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
// testAudio() assumes this sample rate
|
||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||
|
||||
format.setSampleRate(opusConfig->sampleRate);
|
||||
format.setChannelCount(opusConfig->channelCount);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
m_AudioOutput = new QAudioOutput(format);
|
||||
|
||||
// Buffer 40 ms of audio data
|
||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||
|
||||
m_OutputDevice = m_AudioOutput->start();
|
||||
if (m_OutputDevice == nullptr) {
|
||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what buffer size was really used after starting
|
||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
switch (audioConfiguration) {
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
format.setChannelCount(2);
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
format.setChannelCount(6);
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QtAudioRenderer::detectAudioConfiguration()
|
||||
{
|
||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||
|
||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||
|
||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||
if (preferredChannelCount > 2) {
|
||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <QAudioOutput>
|
||||
|
||||
class QtAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
QtAudioRenderer();
|
||||
|
||||
virtual ~QtAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
virtual bool testAudio(int audioConfiguration);
|
||||
|
||||
virtual int detectAudioConfiguration();
|
||||
|
||||
private:
|
||||
QAudioOutput* m_AudioOutput;
|
||||
QIODevice* m_OutputDevice;
|
||||
};
|
||||
@@ -3,43 +3,12 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
SdlAudioRenderer::SdlAudioRenderer()
|
||||
: m_AudioDevice(0),
|
||||
m_AudioBuffer(nullptr)
|
||||
{
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
|
||||
#ifdef HAVE_MMAL
|
||||
// HACK: PulseAudio on Raspberry Pi suffers from horrible underruns,
|
||||
// so switch to ALSA if we detect that we're on a Pi. We need to
|
||||
// actually set a bogus PULSE_SERVER so it doesn't try to get smart on us
|
||||
// and find PA anyway. SDL_AUDIODRIVER=pulseaudio can override this logic.
|
||||
if (qgetenv("SDL_AUDIODRIVER").toLower() != "pulseaudio") {
|
||||
QFile file("/proc/cpuinfo");
|
||||
if (file.open(QIODevice::ReadOnly | QFile::Text)) {
|
||||
QTextStream textStream(&file);
|
||||
if (textStream.readAll().contains("Raspberry Pi")) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding PulseAudio on Raspberry Pi");
|
||||
|
||||
qputenv("PULSE_SERVER", "");
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"ALSA failed; falling back to default");
|
||||
qunsetenv("PULSE_SERVER");
|
||||
}
|
||||
else {
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
@@ -73,7 +42,7 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AudioBuffer = malloc(m_FrameSize);
|
||||
m_AudioBuffer = SDL_malloc(m_FrameSize);
|
||||
if (m_AudioBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to allocate audio buffer");
|
||||
@@ -105,7 +74,7 @@ SdlAudioRenderer::~SdlAudioRenderer()
|
||||
}
|
||||
|
||||
if (m_AudioBuffer != nullptr) {
|
||||
free(m_AudioBuffer);
|
||||
SDL_free(m_AudioBuffer);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
int SdlInputHandler::clipboardThreadProc(void *ptr)
|
||||
{
|
||||
auto me = (SdlInputHandler*)ptr;
|
||||
|
||||
while (!SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||
QString clipboardData;
|
||||
|
||||
SDL_LockMutex(me->m_ClipboardLock);
|
||||
for (;;) {
|
||||
clipboardData = me->m_ClipboardData;
|
||||
me->m_ClipboardData.clear();
|
||||
if (!clipboardData.isEmpty() || SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||
break;
|
||||
}
|
||||
SDL_CondWait(me->m_ClipboardHasData, me->m_ClipboardLock);
|
||||
}
|
||||
SDL_UnlockMutex(me->m_ClipboardLock);
|
||||
|
||||
// We might get here on shutdown, so don't send text if there's
|
||||
// nothing to send.
|
||||
if (!clipboardData.isEmpty()) {
|
||||
me->sendText(clipboardData);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define MAP_KEY(c, sc) \
|
||||
case c: \
|
||||
event.key.keysym.scancode = sc; \
|
||||
break
|
||||
|
||||
#define MAP_KEY_SHIFT(c, sc) \
|
||||
case c: \
|
||||
event.key.keysym.scancode = sc; \
|
||||
event.key.keysym.mod = KMOD_SHIFT; \
|
||||
break
|
||||
|
||||
void SdlInputHandler::sendText(QString& string)
|
||||
{
|
||||
for (int i = 0; i < string.size(); i++) {
|
||||
char16_t c = string[i].unicode();
|
||||
SDL_Event event = {};
|
||||
|
||||
// If we're sending a very long string, we might get a termination request
|
||||
// while we're in the middle of sending a string. In that case, just bail.
|
||||
if (SDL_AtomicGet(&m_ShutdownClipboardThread)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - 'A') + SDL_SCANCODE_A);
|
||||
event.key.keysym.mod = KMOD_SHIFT;
|
||||
}
|
||||
else if (c >= 'a' && c <= 'z') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - 'a') + SDL_SCANCODE_A);
|
||||
}
|
||||
else if (c >= '1' && c <= '9') {
|
||||
event.key.keysym.scancode = (SDL_Scancode)((c - '1') + SDL_SCANCODE_1);
|
||||
}
|
||||
else {
|
||||
// Fixup some Unicode characters to be expressible in ASCII
|
||||
switch (c) {
|
||||
case u'\U0000201C': // Left "
|
||||
case u'\U0000201D': // Right "
|
||||
c = '"';
|
||||
break;
|
||||
|
||||
case u'\U00002018': // Left '
|
||||
case u'\U00002019': // Right '
|
||||
c = '\'';
|
||||
break;
|
||||
|
||||
case u'\U00002013': // Smart -
|
||||
c = '-';
|
||||
break;
|
||||
|
||||
case u'\U00002026': // Ellipsis (...)
|
||||
// Convert this character into 3 periods
|
||||
string.insert(i+1, '.');
|
||||
string.insert(i+1, '.');
|
||||
c = '.';
|
||||
break;
|
||||
|
||||
case u'\U000000A0': // Non-breaking space
|
||||
c = ' ';
|
||||
break;
|
||||
|
||||
default:
|
||||
// Nothing
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
switch (c) {
|
||||
|
||||
// Handle CRLF separately to avoid duplicate newlines
|
||||
case '\r':
|
||||
if (string[i + 1] == '\n') {
|
||||
i++;
|
||||
}
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
|
||||
break;
|
||||
|
||||
MAP_KEY('\b', SDL_SCANCODE_BACKSPACE);
|
||||
MAP_KEY('\n', SDL_SCANCODE_RETURN);
|
||||
MAP_KEY('\t', SDL_SCANCODE_TAB);
|
||||
|
||||
MAP_KEY(' ', SDL_SCANCODE_SPACE);
|
||||
MAP_KEY_SHIFT('!', SDL_SCANCODE_1);
|
||||
MAP_KEY_SHIFT('"', SDL_SCANCODE_APOSTROPHE);
|
||||
MAP_KEY_SHIFT('#', SDL_SCANCODE_3);
|
||||
MAP_KEY_SHIFT('$', SDL_SCANCODE_4);
|
||||
MAP_KEY_SHIFT('%', SDL_SCANCODE_5);
|
||||
MAP_KEY_SHIFT('&', SDL_SCANCODE_7);
|
||||
MAP_KEY('\'', SDL_SCANCODE_APOSTROPHE);
|
||||
MAP_KEY_SHIFT('(', SDL_SCANCODE_9);
|
||||
MAP_KEY_SHIFT(')', SDL_SCANCODE_0);
|
||||
MAP_KEY_SHIFT('*', SDL_SCANCODE_8);
|
||||
MAP_KEY_SHIFT('+', SDL_SCANCODE_EQUALS);
|
||||
MAP_KEY(',', SDL_SCANCODE_COMMA);
|
||||
MAP_KEY('-', SDL_SCANCODE_MINUS);
|
||||
MAP_KEY('.', SDL_SCANCODE_PERIOD);
|
||||
MAP_KEY('/', SDL_SCANCODE_SLASH);
|
||||
MAP_KEY('0', SDL_SCANCODE_0);
|
||||
|
||||
MAP_KEY_SHIFT(':', SDL_SCANCODE_SEMICOLON);
|
||||
MAP_KEY(';', SDL_SCANCODE_SEMICOLON);
|
||||
MAP_KEY_SHIFT('<', SDL_SCANCODE_COMMA);
|
||||
MAP_KEY('=', SDL_SCANCODE_EQUALS);
|
||||
MAP_KEY_SHIFT('>', SDL_SCANCODE_PERIOD);
|
||||
MAP_KEY_SHIFT('?', SDL_SCANCODE_SLASH);
|
||||
MAP_KEY_SHIFT('@', SDL_SCANCODE_2);
|
||||
|
||||
MAP_KEY('[', SDL_SCANCODE_LEFTBRACKET);
|
||||
MAP_KEY('\\', SDL_SCANCODE_BACKSLASH);
|
||||
MAP_KEY(']', SDL_SCANCODE_RIGHTBRACKET);
|
||||
MAP_KEY_SHIFT('^', SDL_SCANCODE_6);
|
||||
MAP_KEY_SHIFT('_', SDL_SCANCODE_MINUS);
|
||||
MAP_KEY('`', SDL_SCANCODE_GRAVE);
|
||||
|
||||
MAP_KEY_SHIFT('{', SDL_SCANCODE_LEFTBRACKET);
|
||||
MAP_KEY_SHIFT('|', SDL_SCANCODE_BACKSLASH);
|
||||
MAP_KEY_SHIFT('}', SDL_SCANCODE_RIGHTBRACKET);
|
||||
MAP_KEY_SHIFT('~', SDL_SCANCODE_GRAVE);
|
||||
|
||||
default:
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Pasting text - non-ASCII character value 'U+%x' ignored",
|
||||
c);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||
SDL_Event modifierEvent = {};
|
||||
modifierEvent.type = SDL_KEYDOWN;
|
||||
modifierEvent.key.state = SDL_PRESSED;
|
||||
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||
handleKeyEvent(&modifierEvent.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
}
|
||||
|
||||
event.type = SDL_KEYDOWN;
|
||||
event.key.state = SDL_PRESSED;
|
||||
handleKeyEvent(&event.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
|
||||
event.type = SDL_KEYUP;
|
||||
event.key.state = SDL_RELEASED;
|
||||
handleKeyEvent(&event.key);
|
||||
|
||||
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||
SDL_Event modifierEvent = {};
|
||||
modifierEvent.type = SDL_KEYUP;
|
||||
modifierEvent.key.state = SDL_RELEASED;
|
||||
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||
handleKeyEvent(&modifierEvent.key);
|
||||
|
||||
SDL_Delay(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,23 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_SwapFaceButtons) {
|
||||
switch (event->button) {
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
event->button = SDL_CONTROLLER_BUTTON_B;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
event->button = SDL_CONTROLLER_BUTTON_A;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
event->button = SDL_CONTROLLER_BUTTON_Y;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
event->button = SDL_CONTROLLER_BUTTON_X;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->state == SDL_PRESSED) {
|
||||
state->buttons |= k_ButtonMap[event->button];
|
||||
|
||||
@@ -252,7 +269,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Clear buttons down on this gameapd
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Select+L1+R1+X as a gamepad overlay combo
|
||||
if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | X_FLAG)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle gamepad combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
|
||||
+221
-23
@@ -3,9 +3,11 @@
|
||||
#include "streaming/session.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
#include "path.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define MOUSE_POLLING_INTERVAL 5
|
||||
|
||||
@@ -13,25 +15,34 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
: m_MultiController(prefs.multiController),
|
||||
m_GamepadMouse(prefs.gamepadMouse),
|
||||
m_SwapMouseButtons(prefs.swapMouseButtons),
|
||||
m_ReverseScrollDirection(prefs.reverseScrollDirection),
|
||||
m_SwapFaceButtons(prefs.swapFaceButtons),
|
||||
m_MouseMoveTimer(0),
|
||||
m_MousePositionLock(0),
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_FakeCaptureActive(false),
|
||||
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
|
||||
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
|
||||
m_LongPressTimer(0),
|
||||
m_StreamWidth(streamWidth),
|
||||
m_StreamHeight(streamHeight),
|
||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||
m_PendingMouseLeaveButtonUp(0),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
m_DragButton(0),
|
||||
m_NumFingersDown(0)
|
||||
m_NumFingersDown(0),
|
||||
m_ClipboardData()
|
||||
{
|
||||
// Allow gamepad input when the app doesn't have focus
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
// System keys are always captured when running without a DE
|
||||
if (!WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS;
|
||||
}
|
||||
|
||||
// Allow gamepad input when the app doesn't have focus if requested
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
||||
|
||||
// If absolute mouse mode is enabled, use relative mode warp (which
|
||||
// is via normal motion events that are influenced by mouse acceleration).
|
||||
@@ -41,28 +52,69 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
prefs.absoluteMouseMode ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD
|
||||
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
||||
m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
#endif
|
||||
|
||||
// Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled
|
||||
SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0");
|
||||
|
||||
// Allow clicks to pass through to us when focusing the window. If we're in
|
||||
// absolute mouse mode, this will avoid the user having to click twice to
|
||||
// trigger a click on the host if the Moonlight window is not focused. In
|
||||
// relative mode, the click event will trigger the mouse to be recaptured.
|
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
|
||||
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
|
||||
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
|
||||
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
|
||||
// SDL 2.0.10 where the bug is fixed.
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/133
|
||||
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
// Enabling extended input reports allows rumble to function on Bluetooth PS4
|
||||
// Enabling extended input reports allows rumble to function on Bluetooth PS4/PS5
|
||||
// controllers, but breaks DirectInput applications. We will enable it because
|
||||
// it's likely that working rumble is what the user is expecting. If they don't
|
||||
// want this behavior, they can override it with the environment variable.
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
#endif
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||
|
||||
// Populate special key combo configuration
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
@@ -120,6 +172,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSGetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), &m_OldHotKeyMode);
|
||||
#endif
|
||||
|
||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||
// during stream startup as we detect currently attached gamepads one at a time.
|
||||
@@ -146,6 +202,13 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
}
|
||||
|
||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||
|
||||
// Initialize state used by the clipboard thread before we start it
|
||||
SDL_AtomicSet(&m_ShutdownClipboardThread, 0);
|
||||
m_ClipboardHasData = SDL_CreateCond();
|
||||
m_ClipboardLock = SDL_CreateMutex();
|
||||
|
||||
m_ClipboardThread = SDL_CreateThread(SdlInputHandler::clipboardThreadProc, "Clipboard Sender", this);
|
||||
}
|
||||
|
||||
SdlInputHandler::~SdlInputHandler()
|
||||
@@ -171,6 +234,21 @@ SdlInputHandler::~SdlInputHandler()
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
|
||||
// Wake up the clipboard thread to terminate it
|
||||
SDL_AtomicSet(&m_ShutdownClipboardThread, 1);
|
||||
SDL_CondBroadcast(m_ClipboardHasData);
|
||||
|
||||
// Wait for it to terminate
|
||||
SDL_WaitThread(m_ClipboardThread, nullptr);
|
||||
|
||||
// Now we can safely clean up its resources
|
||||
SDL_DestroyCond(m_ClipboardHasData);
|
||||
SDL_DestroyMutex(m_ClipboardLock);
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||
@@ -211,7 +289,7 @@ void SdlInputHandler::raiseAllKeys()
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Raising %d keys",
|
||||
m_KeysDown.count());
|
||||
(int)m_KeysDown.count());
|
||||
|
||||
for (auto keyDown : m_KeysDown) {
|
||||
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
||||
@@ -222,20 +300,22 @@ void SdlInputHandler::raiseAllKeys()
|
||||
|
||||
void SdlInputHandler::notifyMouseLeave()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to
|
||||
// allow us to receive the mouse button up events later.
|
||||
//
|
||||
// On macOS and X11, capturing the mouse allows us to receive mouse motion outside the
|
||||
// window (button up already worked without capture).
|
||||
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||
if (mouseState & SDL_BUTTON(button)) {
|
||||
m_PendingMouseLeaveButtonUp = button;
|
||||
SDL_CaptureMouse(SDL_TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusLost()
|
||||
@@ -248,11 +328,27 @@ void SdlInputHandler::notifyFocusLost()
|
||||
setCaptureActive(false);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Ungrab the keyboard
|
||||
updateKeyboardGrabState();
|
||||
#endif
|
||||
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
raiseAllKeys();
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusGained()
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Re-grab the keyboard if it was grabbed before focus loss
|
||||
// FIXME: We only do this on macOS because we get a spurious
|
||||
// focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows
|
||||
// immediately after losing focus by clicking on another window.
|
||||
updateKeyboardGrabState();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isCaptureActive()
|
||||
{
|
||||
if (SDL_GetRelativeMouseMode()) {
|
||||
@@ -263,12 +359,106 @@ bool SdlInputHandler::isCaptureActive()
|
||||
return m_FakeCaptureActive;
|
||||
}
|
||||
|
||||
void SdlInputHandler::updateKeyboardGrabState()
|
||||
{
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldGrab = isCaptureActive();
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
// Ungrab if it's fullscreen only and we left fullscreen
|
||||
shouldGrab = false;
|
||||
}
|
||||
#ifdef Q_OS_DARWIN
|
||||
else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) {
|
||||
// Ungrab if we lose input focus on macOS. SDL will handle
|
||||
// this internally for platforms where we use the SDL
|
||||
// SDL_SetWindowKeyboardGrab() API exclusively.
|
||||
//
|
||||
// NB: On X11, we may not have input focus at the time of
|
||||
// the initial keyboard grab update, so we must not enable
|
||||
// this codepath on Linux.
|
||||
shouldGrab = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Don't close the window on Alt+F4 when keyboard grab is enabled
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0");
|
||||
|
||||
if (shouldGrab) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland.
|
||||
// This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag
|
||||
// that we look for to see if keyboard capture is enabled. We'll handle macOS
|
||||
// ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API.
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
// If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet,
|
||||
// grab the cursor (will grab the keyboard too on X11).
|
||||
if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) {
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
#endif
|
||||
#ifdef Q_OS_DARWIN
|
||||
// SDL doesn't support this private macOS API
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the keyboard to leave the window
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
#ifdef Q_OS_DARWIN
|
||||
// SDL doesn't support this private macOS API
|
||||
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
{
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|| !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED)
|
||||
#else
|
||||
|| !(windowFlags & SDL_WINDOW_INPUT_GRABBED)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdlInputHandler::setCaptureActive(bool active)
|
||||
{
|
||||
if (active) {
|
||||
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!m_AbsoluteMouseMode) {
|
||||
@@ -282,7 +472,7 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
m_FakeCaptureActive = true;
|
||||
}
|
||||
|
||||
@@ -315,9 +505,17 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
|
||||
#else
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Now update the keyboard grab
|
||||
updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#define SDL_CODE_HIDE_CURSOR 1
|
||||
#define SDL_CODE_SHOW_CURSOR 2
|
||||
#define SDL_CODE_UNCAPTURE_MOUSE 3
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
@@ -28,6 +29,25 @@ struct GamepadState {
|
||||
unsigned char lt, rt;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include <CoreGraphics/CGError.h>
|
||||
extern "C" {
|
||||
typedef int CGSConnection;
|
||||
typedef enum {
|
||||
CGSGlobalHotKeyEnable = 0,
|
||||
CGSGlobalHotKeyDisable = 1,
|
||||
} CGSGlobalHotKeyOperatingMode;
|
||||
|
||||
extern CGSConnection _CGSDefaultConnection(void);
|
||||
|
||||
extern CGError CGSGetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||
CGSGlobalHotKeyOperatingMode* mode);
|
||||
|
||||
extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||
CGSGlobalHotKeyOperatingMode mode);
|
||||
}
|
||||
#endif
|
||||
|
||||
#define MAX_GAMEPADS 4
|
||||
#define MAX_FINGERS 2
|
||||
|
||||
@@ -64,6 +84,8 @@ public:
|
||||
|
||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||
|
||||
void sendText(QString& string);
|
||||
|
||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||
|
||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||
@@ -76,8 +98,12 @@ public:
|
||||
|
||||
void notifyFocusLost();
|
||||
|
||||
void notifyFocusGained();
|
||||
|
||||
bool isCaptureActive();
|
||||
|
||||
bool isSystemKeyCaptureActive();
|
||||
|
||||
void setCaptureActive(bool active);
|
||||
|
||||
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
|
||||
@@ -86,10 +112,24 @@ public:
|
||||
|
||||
void flushMousePositionUpdate();
|
||||
|
||||
void updateKeyboardGrabState();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
private:
|
||||
enum KeyCombo {
|
||||
KeyComboQuit,
|
||||
KeyComboUngrabInput,
|
||||
KeyComboToggleFullScreen,
|
||||
KeyComboToggleStatsOverlay,
|
||||
KeyComboToggleMouseMode,
|
||||
KeyComboToggleCursorHide,
|
||||
KeyComboToggleMinimize,
|
||||
KeyComboPasteText,
|
||||
KeyComboMax
|
||||
};
|
||||
|
||||
GamepadState*
|
||||
findStateForGamepad(SDL_JoystickID id);
|
||||
|
||||
@@ -99,6 +139,8 @@ private:
|
||||
|
||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
void performSpecialKeyCombo(KeyCombo combo);
|
||||
|
||||
static
|
||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
@@ -117,10 +159,15 @@ private:
|
||||
static
|
||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
int clipboardThreadProc(void *ptr);
|
||||
|
||||
SDL_Window* m_Window;
|
||||
bool m_MultiController;
|
||||
bool m_GamepadMouse;
|
||||
bool m_SwapMouseButtons;
|
||||
bool m_ReverseScrollDirection;
|
||||
bool m_SwapFaceButtons;
|
||||
SDL_TimerID m_MouseMoveTimer;
|
||||
SDL_atomic_t m_MouseDeltaX;
|
||||
SDL_atomic_t m_MouseDeltaY;
|
||||
@@ -140,6 +187,19 @@ private:
|
||||
bool m_FakeCaptureActive;
|
||||
QString m_OldIgnoreDevices;
|
||||
QString m_OldIgnoreDevicesExcept;
|
||||
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
||||
int m_MouseCursorCapturedVisibilityState;
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
CGSGlobalHotKeyOperatingMode m_OldHotKeyMode;
|
||||
#endif
|
||||
|
||||
struct {
|
||||
KeyCombo keyCombo;
|
||||
SDL_Keycode keyCode;
|
||||
SDL_Scancode scanCode;
|
||||
bool enabled;
|
||||
} m_SpecialKeyCombos[KeyComboMax];
|
||||
|
||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||
@@ -148,7 +208,6 @@ private:
|
||||
int m_StreamHeight;
|
||||
bool m_AbsoluteMouseMode;
|
||||
bool m_AbsoluteTouchMode;
|
||||
Uint32 m_PendingMouseLeaveButtonUp;
|
||||
|
||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||
@@ -157,5 +216,11 @@ private:
|
||||
char m_DragButton;
|
||||
int m_NumFingersDown;
|
||||
|
||||
SDL_Thread* m_ClipboardThread;
|
||||
SDL_atomic_t m_ShutdownClipboardThread;
|
||||
QString m_ClipboardData;
|
||||
SDL_cond* m_ClipboardHasData;
|
||||
SDL_mutex* m_ClipboardLock;
|
||||
|
||||
static const int k_ButtonMap[];
|
||||
};
|
||||
|
||||
+152
-150
@@ -3,12 +3,6 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
// Until we can fully capture these on all platforms (without conflicting with
|
||||
// OS-provided shortcuts), we should avoid passing them through to the host.
|
||||
//#define ENABLE_META
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
|
||||
@@ -19,13 +13,134 @@
|
||||
#define VK_NUMPAD0 0x60
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||
{
|
||||
switch (combo) {
|
||||
case KeyComboQuit:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
break;
|
||||
|
||||
case KeyComboUngrabInput:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleFullScreen:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleStatsOverlay:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
break;
|
||||
|
||||
case KeyComboToggleMouseMode:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
break;
|
||||
|
||||
case KeyComboToggleCursorHide:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState;
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyComboToggleMinimize:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected minimize combo");
|
||||
SDL_MinimizeWindow(m_Window);
|
||||
break;
|
||||
|
||||
case KeyComboPasteText:
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected type clipboard text combo");
|
||||
|
||||
// Force raise all keys to ensure that none of them interfere
|
||||
// with the text we're going to type.
|
||||
raiseAllKeys();
|
||||
|
||||
const char* text;
|
||||
if (SDL_HasClipboardText() && (text = SDL_GetClipboardText()) != nullptr) {
|
||||
// Append this data to the clipboard data string for the thread to process
|
||||
SDL_LockMutex(m_ClipboardLock);
|
||||
m_ClipboardData.append(text);
|
||||
SDL_CondSignal(m_ClipboardHasData);
|
||||
SDL_UnlockMutex(m_ClipboardLock);
|
||||
|
||||
// SDL_GetClipboardText() allocates, so we must free
|
||||
SDL_free((void*)text);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No text in clipboard to paste!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
#define IS_SYNTHETIC_KEY_EVENT(x) ((x)->timestamp == 0)
|
||||
|
||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
{
|
||||
short keyCode;
|
||||
char modifiers;
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for our special key combos
|
||||
if ((event->state == SDL_PRESSED) &&
|
||||
// Ignore timestamp == 0 which are sent from our keyboard code.
|
||||
if (!IS_SYNTHETIC_KEY_EVENT(event) &&
|
||||
(event->state == SDL_PRESSED) &&
|
||||
(event->keysym.mod & KMOD_CTRL) &&
|
||||
(event->keysym.mod & KMOD_ALT) &&
|
||||
(event->keysym.mod & KMOD_SHIFT)) {
|
||||
@@ -40,140 +155,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
// where the SDLK for one shortcut collides with
|
||||
// the scancode of another.
|
||||
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
if (event->keysym.sym == SDLK_q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (SDLK)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.sym == m_SpecialKeyCombos[i].keyCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (SDLK)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.scancode == m_SpecialKeyCombos[i].scanCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (SDLK)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (SDLK)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
||||
else if (event->keysym.sym == SDLK_s) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (SDLK)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (scancode)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (scancode)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (scancode)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (scancode)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (scancode)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set modifier flags
|
||||
@@ -187,11 +181,11 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
if (event->keysym.mod & KMOD_SHIFT) {
|
||||
modifiers |= MODIFIER_SHIFT;
|
||||
}
|
||||
#ifdef ENABLE_META
|
||||
if (event->keysym.mod & KMOD_GUI) {
|
||||
modifiers |= MODIFIER_META;
|
||||
if (isSystemKeyCaptureActive()) {
|
||||
modifiers |= MODIFIER_META;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
||||
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
||||
@@ -332,14 +326,18 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
case SDL_SCANCODE_RALT:
|
||||
keyCode = 0xA5;
|
||||
break;
|
||||
#ifdef ENABLE_META
|
||||
case SDL_SCANCODE_LGUI:
|
||||
if (!isSystemKeyCaptureActive()) {
|
||||
return;
|
||||
}
|
||||
keyCode = 0x5B;
|
||||
break;
|
||||
case SDL_SCANCODE_RGUI:
|
||||
if (!isSystemKeyCaptureActive()) {
|
||||
return;
|
||||
}
|
||||
keyCode = 0x5C;
|
||||
break;
|
||||
#endif
|
||||
case SDL_SCANCODE_AC_BACK:
|
||||
keyCode = 0xA6;
|
||||
break;
|
||||
@@ -405,15 +403,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
// If this is a synthetic keypress from the clipboard code,
|
||||
// this will be on a non-main thread, so don't touch m_KeysDown.
|
||||
if (!IS_SYNTHETIC_KEY_EVENT(event)) {
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(keyCode,
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
event->state == SDL_PRESSED ?
|
||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifiers);
|
||||
|
||||
@@ -133,7 +133,15 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
||||
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||
if (buttonState == 0) {
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
if (m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
// Tell the main thread to stop capturing the mouse now
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = SDL_CODE_UNCAPTURE_MOUSE;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
}
|
||||
}
|
||||
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||
@@ -146,7 +154,8 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// executes.
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
event.user.code = (mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ?
|
||||
SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
if (!mouseInVideoRegion && buttonState != 0) {
|
||||
@@ -204,6 +213,11 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
}
|
||||
|
||||
if (event->y != 0) {
|
||||
// Invert the scroll direction if needed
|
||||
if (m_ReverseScrollDirection) {
|
||||
event->y = -event->y;
|
||||
}
|
||||
|
||||
LiSendScrollEvent((signed char)event->y);
|
||||
}
|
||||
}
|
||||
@@ -245,33 +259,5 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||
// Send mouse position updates if applicable
|
||||
me->flushMousePositionUpdate();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// See comment in SdlInputHandler::notifyMouseLeave()
|
||||
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
|
||||
int mouseX, mouseY;
|
||||
int windowX, windowY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
|
||||
|
||||
// If the button is now up, send the synthetic mouse up event
|
||||
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
|
||||
SDL_Event event;
|
||||
|
||||
event.button.type = SDL_MOUSEBUTTONUP;
|
||||
event.button.timestamp = SDL_GetTicks();
|
||||
event.button.windowID = SDL_GetWindowID(me->m_Window);
|
||||
event.button.which = 0;
|
||||
event.button.button = me->m_PendingMouseLeaveButtonUp;
|
||||
event.button.state = SDL_RELEASED;
|
||||
event.button.clicks = 1;
|
||||
event.button.x = mouseX - windowX;
|
||||
event.button.y = mouseY - windowY;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
me->m_PendingMouseLeaveButtonUp = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
+211
-89
@@ -22,6 +22,8 @@
|
||||
#define ICON_SIZE 64
|
||||
#endif
|
||||
|
||||
#define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100
|
||||
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include <QtEndian>
|
||||
@@ -35,14 +37,6 @@
|
||||
|
||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||
|
||||
// Running the connection process asynchronously seems to reliably
|
||||
// cause a crash in QSGRenderThread on Wayland and strange crashes
|
||||
// elsewhere. Until these are figured out, avoid the async connect
|
||||
// thread on Linux and BSDs.
|
||||
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
|
||||
#define USE_ASYNC_CONNECT_THREAD 1
|
||||
#endif
|
||||
|
||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clStageStarting,
|
||||
nullptr,
|
||||
@@ -63,29 +57,23 @@ void Session::clStageStarting(int stage)
|
||||
// which happens to be the main thread, so it's cool to interact
|
||||
// with the GUI in these callbacks.
|
||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clStageFailed(int stage, int errorCode)
|
||||
{
|
||||
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromStage(stage));
|
||||
unsigned int portFlags = LiGetPortFlagsFromStage(stage);
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
char failingPorts[128];
|
||||
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
|
||||
}
|
||||
|
||||
void Session::clConnectionTerminated(int errorCode)
|
||||
{
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromTerminationErrorCode(errorCode));
|
||||
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
// Display the termination dialog if this was not intended
|
||||
switch (errorCode) {
|
||||
@@ -94,17 +82,29 @@ void Session::clConnectionTerminated(int errorCode)
|
||||
|
||||
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError("No video received from host. Check the host PC's firewall and port forwarding rules.");
|
||||
|
||||
char ports[128];
|
||||
SDL_assert(portFlags != 0);
|
||||
LiStringifyPortFlags(portFlags, ", ", ports, sizeof(ports));
|
||||
emit s_ActiveSession->displayLaunchError(tr("No video received from host.") + "\n\n"+
|
||||
tr("Check your firewall and port forwarding rules for port(s): %1").arg(ports));
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_FRAME:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.");
|
||||
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_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" +
|
||||
tr("Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.") + "\n\n" +
|
||||
tr("If the issue persists, try reinstalling your GPU drivers and GeForce Experience."));
|
||||
break;
|
||||
|
||||
default:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||
emit s_ActiveSession->displayLaunchError(tr("Connection terminated"));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
{
|
||||
DECODER_PARAMETERS params;
|
||||
|
||||
// We should never have vsync enabled for test-mode.
|
||||
// It introduces unnecessary delay for renderers that may
|
||||
// block while waiting for a backbuffer swap.
|
||||
SDL_assert(!enableVsync || !testOnly);
|
||||
|
||||
params.width = width;
|
||||
params.height = height;
|
||||
params.frameRate = frameRate;
|
||||
@@ -292,7 +297,7 @@ void Session::getDecoderInfo(SDL_Window* window,
|
||||
|
||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = isFullScreenOnly = false;
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +315,7 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -332,7 +337,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -340,6 +345,10 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
|
||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||
|
||||
if (decoder->isAlwaysFullScreen()) {
|
||||
m_IsFullScreen = true;
|
||||
}
|
||||
|
||||
delete decoder;
|
||||
|
||||
return true;
|
||||
@@ -347,6 +356,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
|
||||
m_Computer(computer),
|
||||
m_App(app),
|
||||
m_Window(nullptr),
|
||||
@@ -354,6 +364,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_DecoderLock(0),
|
||||
m_NeedsIdr(false),
|
||||
m_AudioDisabled(false),
|
||||
m_AudioMuted(false),
|
||||
m_DisplayOriginX(0),
|
||||
m_DisplayOriginY(0),
|
||||
m_PendingWindowedTransition(false),
|
||||
@@ -361,6 +372,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_InputHandler(nullptr),
|
||||
m_InputHandlerLock(0),
|
||||
m_MouseEmulationRefCount(0),
|
||||
m_FlushingWindowEventsRef(0),
|
||||
m_AsyncConnectionSuccess(false),
|
||||
m_PortTestResults(0),
|
||||
m_OpusDecoder(nullptr),
|
||||
@@ -421,6 +433,12 @@ bool Session::initialize()
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Enable audio encryption as long as we're not on Steam Link.
|
||||
// That hardware can hardly handle Opus decoding at all.
|
||||
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
|
||||
#endif
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Video bitrate: %d kbps",
|
||||
m_StreamConfig.bitrate);
|
||||
@@ -504,8 +522,12 @@ bool Session::initialize()
|
||||
{
|
||||
default:
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
// Only use full-screen desktop mode if we're running a desktop environment
|
||||
if (WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
}
|
||||
// Fall-through
|
||||
case StreamingPreferences::WM_FULLSCREEN:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
break;
|
||||
@@ -552,10 +574,13 @@ void Session::emitLaunchWarning(QString text)
|
||||
// to allow it to transition off the screen before continuing.
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
||||
// Pump the UI loop while we wait
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
if (!m_ThreadedExec) {
|
||||
// Pump the UI loop while we wait if we're on the main thread
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,25 +588,30 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
{
|
||||
QStringList warningList;
|
||||
|
||||
if (!m_Computer->isSupportedServerVersion) {
|
||||
emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
||||
emitLaunchWarning("Your selection to enable remote desktop mouse mode may cause problems in games.");
|
||||
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
||||
}
|
||||
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
|
||||
emitLaunchWarning("HDR is not supported with software decoding.");
|
||||
emitLaunchWarning(tr("HDR is not supported with software decoding."));
|
||||
m_StreamConfig.enableHdr = false;
|
||||
}
|
||||
else {
|
||||
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||
emitLaunchWarning(tr("Your settings selection to force software decoding may cause poor streaming performance."));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||
emitLaunchWarning(tr("Using unsupported FPS options may cause stuttering or lag."));
|
||||
|
||||
if (m_Preferences->enableVsync) {
|
||||
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||
emitLaunchWarning(tr("V-sync will be disabled when streaming at a higher frame rate than the display."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,18 +628,18 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||
emitLaunchWarning(tr("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance."));
|
||||
}
|
||||
else {
|
||||
emitLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
|
||||
emitLaunchWarning(tr("This PC's GPU doesn't support HEVC decoding."));
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hevcForced) {
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||
emitLaunchWarning("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming."));
|
||||
|
||||
// Moonlight-common-c will handle this case already, but we want
|
||||
// to set this explicitly here so we can do our hardware acceleration
|
||||
@@ -625,12 +655,12 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
|
||||
// Check that the app supports HDR
|
||||
if (!m_App.hdrSupported) {
|
||||
emitLaunchWarning(m_App.name + " doesn't support HDR10.");
|
||||
emitLaunchWarning(tr("%1 doesn't support HDR10.").arg(m_App.name));
|
||||
}
|
||||
// Check that the server GPU supports HDR
|
||||
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||
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."));
|
||||
}
|
||||
else if (!isHardwareDecodeAvailable(testWindow,
|
||||
m_Preferences->videoDecoderSelection,
|
||||
@@ -638,7 +668,7 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
emitLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
|
||||
emitLaunchWarning(tr("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming."));
|
||||
}
|
||||
else {
|
||||
// TODO: Also validate display capabilites
|
||||
@@ -651,7 +681,7 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
if (m_StreamConfig.width >= 3840) {
|
||||
// Only allow 4K on GFE 3.x+
|
||||
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||
emitLaunchWarning(tr("GeForce Experience 3.0 or higher is required for 4K streaming."));
|
||||
|
||||
m_StreamConfig.width = 1920;
|
||||
m_StreamConfig.height = 1080;
|
||||
@@ -666,29 +696,31 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
|
||||
if (audioTestPassed) {
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
emitLaunchWarning("Your selected surround sound setting is not supported by the current audio device.");
|
||||
emitLaunchWarning(tr("Your selected surround sound setting is not supported by the current audio device."));
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing worked, warn the user that audio will not work
|
||||
m_AudioDisabled = !audioTestPassed;
|
||||
if (m_AudioDisabled) {
|
||||
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||
emitLaunchWarning(tr("Failed to open audio device. Audio will be unavailable during this session."));
|
||||
}
|
||||
|
||||
// Check for unmapped gamepads
|
||||
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
|
||||
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
|
||||
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) {
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||
emit displayLaunchError("Your host PC's GPU doesn't support streaming video resolutions over 4K.");
|
||||
// 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)) {
|
||||
emit displayLaunchError(tr("Your host PC's GPU doesn't support streaming video resolutions over 4K."));
|
||||
return false;
|
||||
}
|
||||
else if (!m_StreamConfig.supportsHevc) {
|
||||
emit displayLaunchError("Video resolutions over 4K are only supported by the HEVC codec.");
|
||||
emit displayLaunchError(tr("Video resolutions over 4K are only supported by the HEVC codec."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -702,10 +734,10 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||
emit displayLaunchError(tr("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU."));
|
||||
}
|
||||
else {
|
||||
emit displayLaunchError("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.");
|
||||
emit displayLaunchError(tr("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec."));
|
||||
}
|
||||
|
||||
// Fail the launch, because we won't manage to get a decoder for the actual stream
|
||||
@@ -804,7 +836,7 @@ void Session::getWindowDimensions(int& x, int& y,
|
||||
}
|
||||
}
|
||||
|
||||
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
||||
fullScreen = m_IsFullScreen;
|
||||
}
|
||||
|
||||
SDL_Rect usableBounds;
|
||||
@@ -914,7 +946,7 @@ void Session::toggleFullscreen()
|
||||
|
||||
if (fullScreen) {
|
||||
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
|
||||
// Confine the cursor to the window if we're capturing input
|
||||
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
@@ -931,6 +963,9 @@ void Session::toggleFullscreen()
|
||||
// Reposition the window when the resize is complete
|
||||
m_PendingWindowedTransition = true;
|
||||
}
|
||||
|
||||
// Input handler might need to start/stop keyboard grab after changing modes
|
||||
m_InputHandler->updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void Session::notifyMouseEmulationMode(bool enabled)
|
||||
@@ -969,17 +1004,7 @@ bool Session::startConnectionAsync()
|
||||
{
|
||||
// Wait 1.5 seconds before connecting to let the user
|
||||
// have time to read any messages present on the segue
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
SDL_Delay(1500);
|
||||
#else
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||
// Pump the UI loop while we wait since we're not async
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
// The UI should have ensured the old game was already quit
|
||||
// if we decide to stream a different game.
|
||||
@@ -1013,7 +1038,7 @@ bool Session::startConnectionAsync()
|
||||
m_InputHandler->getAttachedGamepadMask());
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
||||
emit displayLaunchError(tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||
return false;
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit displayLaunchError(e.toQString());
|
||||
@@ -1074,11 +1099,75 @@ bool Session::startConnectionAsync()
|
||||
return true;
|
||||
}
|
||||
|
||||
void Session::flushWindowEvents()
|
||||
{
|
||||
// Pump events to ensure all pending OS events are posted
|
||||
SDL_PumpEvents();
|
||||
|
||||
// Insert a barrier to discard any additional window events.
|
||||
// We don't use SDL_FlushEvent() here because it could cause
|
||||
// important events to be lost.
|
||||
m_FlushingWindowEventsRef++;
|
||||
|
||||
// This event will cause us to set m_FlushingWindowEvents back to false.
|
||||
SDL_Event flushEvent = {};
|
||||
flushEvent.type = SDL_USEREVENT;
|
||||
flushEvent.user.code = SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER;
|
||||
SDL_PushEvent(&flushEvent);
|
||||
}
|
||||
|
||||
class ExecThread : public QThread
|
||||
{
|
||||
public:
|
||||
ExecThread(Session* session) :
|
||||
QThread(nullptr),
|
||||
m_Session(session) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Session->execInternal();
|
||||
}
|
||||
|
||||
Session* m_Session;
|
||||
};
|
||||
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
|
||||
// Use a separate thread for the streaming session on X11 or Wayland
|
||||
// to ensure we don't stomp on Qt's GL context. This breaks when using
|
||||
// the Qt EGLFS backend, so we will restrict this to X11
|
||||
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
|
||||
|
||||
if (m_ThreadedExec) {
|
||||
// Run the streaming session on a separate thread for Linux/BSD
|
||||
ExecThread execThread(this);
|
||||
execThread.start();
|
||||
|
||||
// Until the SDL streaming window is created, we should continue
|
||||
// to update the Qt UI to allow warning messages to display and
|
||||
// make sure that the Qt window can hide itself.
|
||||
while (!execThread.wait(10) && m_Window == nullptr) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
// SDL is in charge now. Wait until the streaming thread exits
|
||||
// to further update the Qt window.
|
||||
execThread.wait();
|
||||
}
|
||||
else {
|
||||
// Run the streaming session on the main thread for Windows and macOS
|
||||
execInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void Session::execInternal()
|
||||
{
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
@@ -1102,22 +1191,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height);
|
||||
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
AsyncConnectionStartThread asyncConnThread(this);
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
if (!m_ThreadedExec) {
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#else
|
||||
asyncConnThread.run();
|
||||
#endif
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
else {
|
||||
// We're already in a separate thread so run the connection operations
|
||||
// synchronously and don't pump the event loop. The main thread is already
|
||||
// pumping the event loop for us.
|
||||
asyncConnThread.run();
|
||||
}
|
||||
|
||||
// If the connection failed, clean up and abort the connection.
|
||||
if (!m_AsyncConnectionSuccess) {
|
||||
@@ -1194,7 +1287,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
if (!m_IsFullScreen) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
// We must set the size before the position because centering
|
||||
@@ -1294,17 +1387,35 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
case SDL_CODE_SHOW_CURSOR:
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
break;
|
||||
case SDL_CODE_UNCAPTURE_MOUSE:
|
||||
SDL_CaptureMouse(SDL_FALSE);
|
||||
break;
|
||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||
m_FlushingWindowEventsRef--;
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||
// Early handling of some events
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
if (m_Preferences->muteOnFocusLoss) {
|
||||
m_AudioMuted = true;
|
||||
}
|
||||
m_InputHandler->notifyFocusLost();
|
||||
}
|
||||
else if (event.window.event == SDL_WINDOWEVENT_LEAVE) {
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
||||
if (m_Preferences->muteOnFocusLoss) {
|
||||
m_AudioMuted = false;
|
||||
}
|
||||
m_InputHandler->notifyFocusGained();
|
||||
break;
|
||||
case SDL_WINDOWEVENT_LEAVE:
|
||||
m_InputHandler->notifyMouseLeave();
|
||||
break;
|
||||
}
|
||||
|
||||
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
|
||||
@@ -1339,6 +1450,16 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
}
|
||||
|
||||
if (m_FlushingWindowEventsRef > 0) {
|
||||
// Ignore window events for renderer reset if flushing
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Dropping window event during flush: %d (%d %d)",
|
||||
event.window.event,
|
||||
event.window.data1,
|
||||
event.window.data2);
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Recreating renderer for window event: %d (%d %d)",
|
||||
event.window.event,
|
||||
@@ -1354,10 +1475,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// Destroy the old decoder
|
||||
delete m_VideoDecoder;
|
||||
|
||||
// Flush any other pending window events that could
|
||||
// send us back here immediately
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||
// Insert a barrier to discard any additional window events
|
||||
// that could cause the renderer to be and recreated again.
|
||||
// We don't use SDL_FlushEvent() here because it could cause
|
||||
// important events to be lost.
|
||||
flushWindowEvents();
|
||||
|
||||
// Update the window display mode based on our current monitor
|
||||
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
@@ -1394,7 +1516,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to recreate decoder after reset");
|
||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||
emit displayLaunchError(tr("Unable to initialize video decoder. Please check your streaming settings and try again."));
|
||||
goto DispatchDeferredCleanup;
|
||||
}
|
||||
|
||||
@@ -1403,7 +1525,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// is set up, this ensures the window re-creation is already done.
|
||||
if (needsPostDecoderCreationCapture) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
needsFirstEnterCapture = false;
|
||||
needsPostDecoderCreationCapture = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -17,6 +17,7 @@ class Session : public QObject
|
||||
friend class SdlInputHandler;
|
||||
friend class DeferredSessionCleanupTask;
|
||||
friend class AsyncConnectionStartThread;
|
||||
friend class ExecThread;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
@@ -39,10 +40,12 @@ public:
|
||||
return m_OverlayManager;
|
||||
}
|
||||
|
||||
void flushWindowEvents();
|
||||
|
||||
signals:
|
||||
void stageStarting(QString stage);
|
||||
|
||||
void stageFailed(QString stage, int errorCode);
|
||||
void stageFailed(QString stage, int errorCode, QString failingPorts);
|
||||
|
||||
void connectionStarted();
|
||||
|
||||
@@ -55,6 +58,8 @@ signals:
|
||||
void sessionFinished(int portTestResult);
|
||||
|
||||
private:
|
||||
void execInternal();
|
||||
|
||||
bool initialize();
|
||||
|
||||
bool startConnectionAsync();
|
||||
@@ -131,6 +136,7 @@ private:
|
||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
StreamingPreferences* m_Preferences;
|
||||
bool m_IsFullScreen;
|
||||
STREAM_CONFIGURATION m_StreamConfig;
|
||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
||||
@@ -141,14 +147,17 @@ private:
|
||||
SDL_SpinLock m_DecoderLock;
|
||||
bool m_NeedsIdr;
|
||||
bool m_AudioDisabled;
|
||||
bool m_AudioMuted;
|
||||
Uint32 m_FullScreenFlag;
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
bool m_ThreadedExec;
|
||||
bool m_PendingWindowedTransition;
|
||||
bool m_UnexpectedTermination;
|
||||
SdlInputHandler* m_InputHandler;
|
||||
SDL_SpinLock m_InputHandlerLock;
|
||||
int m_MouseEmulationRefCount;
|
||||
int m_FlushingWindowEventsRef;
|
||||
|
||||
bool m_AsyncConnectionSuccess;
|
||||
int m_PortTestResults;
|
||||
|
||||
@@ -14,12 +14,24 @@ extern "C" {
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
DrmRenderer::DrmRenderer()
|
||||
: m_DrmFd(-1),
|
||||
: m_HwContext(nullptr),
|
||||
m_DrmFd(-1),
|
||||
m_SdlOwnsDrmFd(false),
|
||||
m_SupportsDirectRendering(false),
|
||||
m_CrtcId(0),
|
||||
m_PlaneId(0),
|
||||
m_CurrentFbId(0)
|
||||
{
|
||||
#ifdef HAVE_EGL
|
||||
m_EGLExtDmaBuf = false;
|
||||
m_eglCreateImage = nullptr;
|
||||
m_eglCreateImageKHR = nullptr;
|
||||
m_eglDestroyImage = nullptr;
|
||||
m_eglDestroyImageKHR = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
DrmRenderer::~DrmRenderer()
|
||||
@@ -28,14 +40,22 @@ DrmRenderer::~DrmRenderer()
|
||||
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
||||
}
|
||||
|
||||
if (m_DrmFd != -1) {
|
||||
if (m_HwContext != nullptr) {
|
||||
av_buffer_unref(&m_HwContext);
|
||||
}
|
||||
|
||||
if (!m_SdlOwnsDrmFd && m_DrmFd != -1) {
|
||||
close(m_DrmFd);
|
||||
}
|
||||
}
|
||||
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
|
||||
{
|
||||
/* Nothing to do */
|
||||
// The out-of-tree LibreELEC patches use this option to control the type of the V4L2
|
||||
// buffers that we get back. We only support NV12 buffers now.
|
||||
av_dict_set_int(options, "pixel_format", AV_PIX_FMT_NV12, 0);
|
||||
|
||||
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using DRM renderer");
|
||||
@@ -43,33 +63,89 @@ bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
bool DrmRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
{
|
||||
const char* device = SDL_getenv("DRM_DEV");
|
||||
int i;
|
||||
|
||||
if (device == nullptr) {
|
||||
device = "/dev/dri/card0";
|
||||
}
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Opening DRM device: %s",
|
||||
device);
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||
if (m_DrmFd < 0) {
|
||||
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open DRM device: %d",
|
||||
errno);
|
||||
"SDL_GetWindowWMInfo() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.subsystem == SDL_SYSWM_KMSDRM) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Sharing DRM FD with SDL");
|
||||
|
||||
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
|
||||
m_DrmFd = info.info.kmsdrm.drm_fd;
|
||||
m_SdlOwnsDrmFd = true;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
const char* device = SDL_getenv("DRM_DEV");
|
||||
|
||||
if (device == nullptr) {
|
||||
device = "/dev/dri/card0";
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Opening DRM device: %s",
|
||||
device);
|
||||
|
||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||
if (m_DrmFd < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open DRM device: %d",
|
||||
errno);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the device context first because it is needed whether we can
|
||||
// actually use direct rendering or not.
|
||||
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_DRM);
|
||||
if (m_HwContext == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"av_hwdevice_ctx_alloc(DRM) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||
AVDRMDeviceContext* drmDeviceContext = (AVDRMDeviceContext*)deviceContext->hwctx;
|
||||
|
||||
drmDeviceContext->fd = m_DrmFd;
|
||||
|
||||
int err = av_hwdevice_ctx_init(m_HwContext);
|
||||
if (err < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"av_hwdevice_ctx_init(DRM) failed: %d",
|
||||
err);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
// Still return true if we fail to initialize DRM direct rendering
|
||||
// stuff, since we have EGL that we can use for indirect rendering.
|
||||
const bool DIRECT_RENDERING_INIT_FAILED = true;
|
||||
#else
|
||||
// Fail if we can't initialize direct rendering and we don't have EGL.
|
||||
const bool DIRECT_RENDERING_INIT_FAILED = false;
|
||||
#endif
|
||||
|
||||
drmModeRes* resources = drmModeGetResources(m_DrmFd);
|
||||
if (resources == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmModeGetResources() failed: %d",
|
||||
errno);
|
||||
return false;
|
||||
"drmModeGetResources() failed: %d",
|
||||
errno);
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Look for a connected connector and get the associated encoder
|
||||
@@ -89,7 +165,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No connected displays found!");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Now find the CRTC from the encoder
|
||||
@@ -109,7 +185,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM encoder not found!");
|
||||
drmModeFreeResources(resources);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
int crtcIndex = -1;
|
||||
@@ -130,7 +206,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
if (crtcIndex == -1) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to get CRTC!");
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
||||
@@ -140,7 +216,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmGetPlaneResources() failed: %d",
|
||||
errno);
|
||||
return false;
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// Find an NV12 overlay plane to render on
|
||||
@@ -185,6 +261,15 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||
|
||||
drmModeFreePlaneResources(planeRes);
|
||||
|
||||
if (m_PlaneId == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to find suitable NV12 overlay plane!");
|
||||
return DIRECT_RENDERING_INIT_FAILED;
|
||||
}
|
||||
|
||||
// If we got this far, we can do direct rendering via the DRM FD.
|
||||
m_SupportsDirectRendering = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -213,6 +298,8 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
uint32_t handles[4] = {};
|
||||
uint32_t pitches[4] = {};
|
||||
uint32_t offsets[4] = {};
|
||||
uint64_t modifiers[4] = {};
|
||||
uint32_t flags = 0;
|
||||
|
||||
SDL_Rect src, dst;
|
||||
|
||||
@@ -234,12 +321,18 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass along the modifiers to DRM if there are some in the descriptor
|
||||
if (drmFrame->objects[0].format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
flags |= DRM_MODE_FB_MODIFIERS;
|
||||
}
|
||||
|
||||
SDL_assert(drmFrame->nb_layers == 1);
|
||||
SDL_assert(drmFrame->layers[0].nb_planes == 2);
|
||||
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
|
||||
handles[i] = primeHandle;
|
||||
pitches[i] = drmFrame->layers[0].planes[i].pitch;
|
||||
offsets[i] = drmFrame->layers[0].planes[i].offset;
|
||||
modifiers[i] = drmFrame->objects[0].format_modifier;
|
||||
}
|
||||
|
||||
// Remember the last FB object we created so we can free it
|
||||
@@ -247,12 +340,15 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
uint32_t lastFbId = m_CurrentFbId;
|
||||
|
||||
// Create a frame buffer object from the PRIME buffer
|
||||
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
|
||||
drmFrame->layers[0].format,
|
||||
handles, pitches, offsets, &m_CurrentFbId, 0);
|
||||
// NB: It is an error to pass modifiers without DRM_MODE_FB_MODIFIERS set.
|
||||
err = drmModeAddFB2WithModifiers(m_DrmFd, frame->width, frame->height,
|
||||
drmFrame->layers[0].format,
|
||||
handles, pitches, offsets,
|
||||
(flags & DRM_MODE_FB_MODIFIERS) ? modifiers : NULL,
|
||||
&m_CurrentFbId, flags);
|
||||
if (err < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"drmModeAddFB2() failed: %d",
|
||||
"drmModeAddFB2WithModifiers() failed: %d",
|
||||
errno);
|
||||
m_CurrentFbId = lastFbId;
|
||||
return;
|
||||
@@ -277,3 +373,200 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
// Free the previous FB object which has now been superseded
|
||||
drmModeRmFB(m_DrmFd, lastFbId);
|
||||
}
|
||||
|
||||
bool DrmRenderer::needsTestFrame()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DrmRenderer::isDirectRenderingSupported()
|
||||
{
|
||||
return m_SupportsDirectRendering;
|
||||
}
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
|
||||
bool DrmRenderer::canExportEGL() {
|
||||
if (qgetenv("DRM_FORCE_DIRECT") == "1") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using direct rendering due to environment variable");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM backend supports exporting EGLImage");
|
||||
return true;
|
||||
}
|
||||
|
||||
AVPixelFormat DrmRenderer::getEGLImagePixelFormat() {
|
||||
// This tells EGLRenderer to treat the EGLImage as a single opaque texture
|
||||
return AV_PIX_FMT_DRM_PRIME;
|
||||
}
|
||||
|
||||
bool DrmRenderer::initializeEGL(EGLDisplay,
|
||||
const EGLExtensions &ext) {
|
||||
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DRM-EGL: DMABUF unsupported");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||
|
||||
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
|
||||
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
|
||||
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
|
||||
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
|
||||
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
|
||||
|
||||
if (!(m_eglCreateImage && m_eglDestroyImage) &&
|
||||
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ssize_t DrmRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
||||
EGLImage images[EGL_MAX_PLANES]) {
|
||||
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||
|
||||
memset(images, 0, sizeof(EGLImage) * EGL_MAX_PLANES);
|
||||
|
||||
SDL_assert(drmFrame->nb_objects == 1);
|
||||
SDL_assert(drmFrame->nb_layers == 1);
|
||||
|
||||
// Max 30 attributes (1 key + 1 value for each)
|
||||
const int MAX_ATTRIB_COUNT = 30 * 2;
|
||||
EGLAttrib attribs[MAX_ATTRIB_COUNT] = {
|
||||
EGL_LINUX_DRM_FOURCC_EXT, (EGLAttrib)drmFrame->layers[0].format,
|
||||
EGL_WIDTH, frame->width,
|
||||
EGL_HEIGHT, frame->height,
|
||||
};
|
||||
int attribIndex = 6;
|
||||
|
||||
for (int i = 0; i < drmFrame->layers[0].nb_planes; ++i) {
|
||||
const auto &plane = drmFrame->layers[0].planes[i];
|
||||
const auto &object = drmFrame->objects[plane.object_index];
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_FD_EXT;
|
||||
attribs[attribIndex++] = object.fd;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_OFFSET_EXT;
|
||||
attribs[attribIndex++] = plane.offset;
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_PITCH_EXT;
|
||||
attribs[attribIndex++] = plane.pitch;
|
||||
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT;
|
||||
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate the attribute list
|
||||
attribs[attribIndex++] = EGL_NONE;
|
||||
SDL_assert(attribIndex <= MAX_ATTRIB_COUNT);
|
||||
|
||||
// Our EGLImages are non-planar, so we only populate the first entry
|
||||
if (m_eglCreateImage) {
|
||||
images[0] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, attribs);
|
||||
if (!images[0]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImage() Failed: %d", eglGetError());
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Cast the EGLAttrib array elements to EGLint for the KHR extension
|
||||
EGLint intAttribs[MAX_ATTRIB_COUNT];
|
||||
for (int i = 0; i < MAX_ATTRIB_COUNT; i++) {
|
||||
intAttribs[i] = (EGLint)attribs[i];
|
||||
}
|
||||
|
||||
images[0] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
|
||||
EGL_LINUX_DMA_BUF_EXT,
|
||||
nullptr, intAttribs);
|
||||
if (!images[0]) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"eglCreateImageKHR() Failed: %d", eglGetError());
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
freeEGLImages(dpy, images);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DrmRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||
if (m_eglDestroyImage) {
|
||||
m_eglDestroyImage(dpy, images[0]);
|
||||
}
|
||||
else {
|
||||
m_eglDestroyImageKHR(dpy, images[0]);
|
||||
}
|
||||
|
||||
// Our EGLImages are non-planar
|
||||
SDL_assert(images[1] == 0);
|
||||
SDL_assert(images[2] == 0);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,12 +14,32 @@ public:
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
virtual int getRendererAttributes() override;
|
||||
virtual bool needsTestFrame() override;
|
||||
virtual bool isDirectRenderingSupported() override;
|
||||
#ifdef HAVE_EGL
|
||||
virtual bool canExportEGL() override;
|
||||
virtual AVPixelFormat getEGLImagePixelFormat() override;
|
||||
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||
#endif
|
||||
|
||||
private:
|
||||
AVBufferRef* m_HwContext;
|
||||
int m_DrmFd;
|
||||
bool m_SdlOwnsDrmFd;
|
||||
bool m_SupportsDirectRendering;
|
||||
uint32_t m_CrtcId;
|
||||
uint32_t m_PlaneId;
|
||||
uint32_t m_CurrentFbId;
|
||||
SDL_Rect m_OutputRect;
|
||||
|
||||
#ifdef HAVE_EGL
|
||||
bool m_EGLExtDmaBuf;
|
||||
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
|
||||
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
|
||||
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
|
||||
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// minwindef.h defines min() and max() macros that conflict with
|
||||
// std::numeric_limits, which Qt uses in some of its headers.
|
||||
#define NOMINMAX
|
||||
|
||||
#include <initguid.h>
|
||||
#include "dxva2.h"
|
||||
#include "../ffmpeg.h"
|
||||
@@ -17,23 +21,31 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
|
||||
|
||||
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
||||
|
||||
typedef struct _VERTEX
|
||||
{
|
||||
float x, y, z, rhw;
|
||||
float tu, tv;
|
||||
} VERTEX, *PVERTEX;
|
||||
|
||||
DXVA2Renderer::DXVA2Renderer() :
|
||||
m_DecService(nullptr),
|
||||
m_Decoder(nullptr),
|
||||
m_SurfacesUsed(0),
|
||||
m_Pool(nullptr),
|
||||
m_OverlayLock(0),
|
||||
m_Device(nullptr),
|
||||
m_RenderTarget(nullptr),
|
||||
m_ProcService(nullptr),
|
||||
m_Processor(nullptr),
|
||||
m_FrameIndex(0),
|
||||
m_DebugOverlayFont(nullptr),
|
||||
m_StatusOverlayFont(nullptr),
|
||||
m_BlockingPresent(false)
|
||||
{
|
||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||
|
||||
RtlZeroMemory(m_OverlayVertexBuffers, sizeof(m_OverlayVertexBuffers));
|
||||
RtlZeroMemory(m_OverlayTextures, sizeof(m_OverlayTextures));
|
||||
|
||||
// Use MMCSS scheduling for lower scheduling latency while we're streaming
|
||||
DwmEnableMMCSS(TRUE);
|
||||
}
|
||||
@@ -48,8 +60,14 @@ DXVA2Renderer::~DXVA2Renderer()
|
||||
SAFE_COM_RELEASE(m_RenderTarget);
|
||||
SAFE_COM_RELEASE(m_ProcService);
|
||||
SAFE_COM_RELEASE(m_Processor);
|
||||
SAFE_COM_RELEASE(m_DebugOverlayFont);
|
||||
SAFE_COM_RELEASE(m_StatusOverlayFont);
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_OverlayVertexBuffers); i++) {
|
||||
SAFE_COM_RELEASE(m_OverlayVertexBuffers[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_OverlayTextures); i++) {
|
||||
SAFE_COM_RELEASE(m_OverlayTextures[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
||||
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
||||
@@ -323,6 +341,23 @@ bool DXVA2Renderer::initializeRenderer()
|
||||
}
|
||||
}
|
||||
|
||||
m_Device->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
|
||||
m_Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
m_Device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
||||
|
||||
m_Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||
m_Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||
|
||||
m_Device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
||||
m_Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||
m_Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||||
|
||||
m_Device->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -347,15 +382,28 @@ bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
|
||||
D3DADAPTER_IDENTIFIER9 id;
|
||||
|
||||
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
|
||||
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||
if (SUCCEEDED(hr)) {
|
||||
if (id.VendorId == 0x8086) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||
|
||||
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||
// platforms.
|
||||
result = true;
|
||||
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||
// platforms.
|
||||
result = true;
|
||||
}
|
||||
else if (id.VendorId == 0x4d4f4351) { // QCOM in ASCII
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Qualcomm GPU");
|
||||
|
||||
// On Qualcomm GPUs (all D3D9on12 GPUs?), the scaling quality of VideoProcessBlt()
|
||||
// is absolutely horrible. StretchRect() is much much better.
|
||||
result = true;
|
||||
}
|
||||
else {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = false;
|
||||
@@ -742,64 +790,201 @@ void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case Overlay::OverlayDebug:
|
||||
if (m_DebugOverlayFont == nullptr) {
|
||||
hr = D3DXCreateFontA(m_Device,
|
||||
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
|
||||
0,
|
||||
FW_HEAVY,
|
||||
1,
|
||||
false,
|
||||
ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE,
|
||||
"",
|
||||
&m_DebugOverlayFont);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DXCreateFontA() failed: %x",
|
||||
hr);
|
||||
m_DebugOverlayFont = nullptr;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Overlay::OverlayStatusUpdate:
|
||||
if (m_StatusOverlayFont == nullptr) {
|
||||
hr = D3DXCreateFontA(m_Device,
|
||||
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
|
||||
0,
|
||||
FW_HEAVY,
|
||||
1,
|
||||
false,
|
||||
ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE,
|
||||
"",
|
||||
&m_StatusOverlayFont);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DXCreateFontA() failed: %x",
|
||||
hr);
|
||||
m_StatusOverlayFont = nullptr;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
SDL_assert(false);
|
||||
break;
|
||||
SDL_Surface* newSurface = Session::get()->getOverlayManager().getUpdatedOverlaySurface(type);
|
||||
if (newSurface == nullptr && Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
// The overlay is enabled and there is no new surface. Leave the old texture alone.
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_AtomicLock(&m_OverlayLock);
|
||||
IDirect3DTexture9* oldTexture = m_OverlayTextures[type];
|
||||
m_OverlayTextures[type] = nullptr;
|
||||
|
||||
IDirect3DVertexBuffer9* oldVertexBuffer = m_OverlayVertexBuffers[type];
|
||||
m_OverlayVertexBuffers[type] = nullptr;
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
|
||||
SAFE_COM_RELEASE(oldTexture);
|
||||
SAFE_COM_RELEASE(oldVertexBuffer);
|
||||
|
||||
// If the overlay is disabled, we're done
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a dynamic texture to populate with our pixel data
|
||||
SDL_assert(!SDL_MUSTLOCK(newSurface));
|
||||
IDirect3DTexture9* newTexture = nullptr;
|
||||
hr = m_Device->CreateTexture(newSurface->w,
|
||||
newSurface->h,
|
||||
1,
|
||||
D3DUSAGE_DYNAMIC,
|
||||
D3DFMT_A8R8G8B8,
|
||||
D3DPOOL_DEFAULT,
|
||||
&newTexture,
|
||||
nullptr);
|
||||
if (FAILED(hr)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateTexture() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
D3DLOCKED_RECT lockedRect;
|
||||
hr = newTexture->LockRect(0, &lockedRect, nullptr, D3DLOCK_DISCARD);
|
||||
if (FAILED(hr)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"IDirect3DTexture9::LockRect() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSurface->pitch == lockedRect.Pitch) {
|
||||
// If the pitch matches, we can take the fast path and use a single copy to transfer the pixels
|
||||
RtlCopyMemory(lockedRect.pBits, newSurface->pixels, newSurface->pitch * newSurface->h);
|
||||
}
|
||||
else {
|
||||
// If the pitch doesn't match, we'll need to copy each row separately
|
||||
int pitch = SDL_min(newSurface->pitch, lockedRect.Pitch);
|
||||
for (int i = 0; i < newSurface->h; i++) {
|
||||
RtlCopyMemory(((PUCHAR)lockedRect.pBits) + (lockedRect.Pitch * i),
|
||||
((PUCHAR)newSurface->pixels) + (newSurface->pitch * i),
|
||||
pitch);
|
||||
}
|
||||
}
|
||||
|
||||
newTexture->UnlockRect(0);
|
||||
|
||||
SDL_FRect renderRect = {};
|
||||
|
||||
if (type == Overlay::OverlayStatusUpdate) {
|
||||
// Bottom Left
|
||||
renderRect.x = 0;
|
||||
renderRect.y = m_DisplayHeight - newSurface->h;
|
||||
}
|
||||
else if (type == Overlay::OverlayDebug) {
|
||||
// Top left
|
||||
renderRect.x = 0;
|
||||
renderRect.y = 0;
|
||||
}
|
||||
|
||||
renderRect.w = newSurface->w;
|
||||
renderRect.h = newSurface->h;
|
||||
|
||||
// The surface is no longer required
|
||||
SDL_FreeSurface(newSurface);
|
||||
newSurface = nullptr;
|
||||
|
||||
VERTEX verts[] =
|
||||
{
|
||||
{renderRect.x, renderRect.y, 0, 1, 0, 0},
|
||||
{renderRect.x, renderRect.y+renderRect.h, 0, 1, 0, 1},
|
||||
{renderRect.x+renderRect.w, renderRect.y+renderRect.h, 0, 1, 1, 1},
|
||||
{renderRect.x+renderRect.w, renderRect.y, 0, 1, 1, 0}
|
||||
};
|
||||
|
||||
IDirect3DVertexBuffer9* newVertexBuffer = nullptr;
|
||||
hr = m_Device->CreateVertexBuffer(sizeof(verts),
|
||||
D3DUSAGE_WRITEONLY,
|
||||
D3DFVF_XYZRHW | D3DFVF_TEX1,
|
||||
D3DPOOL_DEFAULT,
|
||||
&newVertexBuffer,
|
||||
nullptr);
|
||||
if (FAILED(hr)) {
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateVertexBuffer() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
PVOID targetVertexBuffer = nullptr;
|
||||
hr = newVertexBuffer->Lock(0, 0, &targetVertexBuffer, 0);
|
||||
if (FAILED(hr)) {
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SAFE_COM_RELEASE(newVertexBuffer);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"IDirect3DVertexBuffer9::Lock() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
RtlCopyMemory(targetVertexBuffer, verts, sizeof(verts));
|
||||
|
||||
newVertexBuffer->Unlock();
|
||||
|
||||
SDL_AtomicLock(&m_OverlayLock);
|
||||
m_OverlayVertexBuffers[type] = newVertexBuffer;
|
||||
m_OverlayTextures[type] = newTexture;
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
}
|
||||
|
||||
void DXVA2Renderer::renderOverlay(Overlay::OverlayType type)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the overlay is being updated, just skip rendering it this frame
|
||||
if (!SDL_AtomicTryLock(&m_OverlayLock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IDirect3DTexture9* overlayTexture = m_OverlayTextures[type];
|
||||
IDirect3DVertexBuffer9* overlayVertexBuffer = m_OverlayVertexBuffers[type];
|
||||
|
||||
if (overlayTexture == nullptr) {
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reference these objects so they don't immediately go away if the
|
||||
// overlay update thread tries to release them.
|
||||
SDL_assert(overlayVertexBuffer != nullptr);
|
||||
overlayTexture->AddRef();
|
||||
overlayVertexBuffer->AddRef();
|
||||
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
|
||||
hr = m_Device->SetTexture(0, overlayTexture);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SetTexture() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
hr = m_Device->SetStreamSource(0, overlayVertexBuffer, 0, sizeof(VERTEX));
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SetStreamSource() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
hr = m_Device->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DrawPrimitive() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
Exit:
|
||||
overlayTexture->Release();
|
||||
overlayVertexBuffer->Release();
|
||||
}
|
||||
|
||||
int DXVA2Renderer::getDecoderColorspace()
|
||||
{
|
||||
if (isDXVideoProcessorAPIBlacklisted()) {
|
||||
// StretchRect() assumes Rec 601 on Intel GPUs
|
||||
// StretchRect() assumes Rec 601 on Intel and Qualcomm GPUs.
|
||||
return COLORSPACE_REC_601;
|
||||
}
|
||||
else {
|
||||
@@ -998,28 +1183,9 @@ void DXVA2Renderer::renderFrame(AVFrame *frame)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_DebugOverlayFont != nullptr) {
|
||||
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
|
||||
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
|
||||
m_DebugOverlayFont->DrawTextA(nullptr,
|
||||
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
|
||||
-1,
|
||||
&sample.DstRect,
|
||||
DT_LEFT | DT_NOCLIP,
|
||||
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_StatusOverlayFont != nullptr) {
|
||||
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
|
||||
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
|
||||
m_StatusOverlayFont->DrawTextA(nullptr,
|
||||
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
|
||||
-1,
|
||||
&sample.DstRect,
|
||||
DT_RIGHT | DT_NOCLIP,
|
||||
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||
}
|
||||
// Render overlays on top of the video stream
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
renderOverlay((Overlay::OverlayType)i);
|
||||
}
|
||||
|
||||
hr = m_Device->EndScene();
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "pacer/pacer.h"
|
||||
|
||||
#include <d3d9.h>
|
||||
#include <d3dx9.h>
|
||||
#include <dxva2api.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -19,7 +18,7 @@ public:
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType type) override;
|
||||
virtual int getDecoderColorspace() override;
|
||||
|
||||
private:
|
||||
@@ -28,6 +27,7 @@ private:
|
||||
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||
bool isDecoderBlacklisted();
|
||||
bool isDXVideoProcessorAPIBlacklisted();
|
||||
void renderOverlay(Overlay::OverlayType type);
|
||||
|
||||
static
|
||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||
@@ -53,6 +53,10 @@ private:
|
||||
int m_SurfacesUsed;
|
||||
AVBufferPool* m_Pool;
|
||||
|
||||
SDL_SpinLock m_OverlayLock;
|
||||
IDirect3DVertexBuffer9* m_OverlayVertexBuffers[Overlay::OverlayMax];
|
||||
IDirect3DTexture9* m_OverlayTextures[Overlay::OverlayMax];
|
||||
|
||||
IDirect3DDevice9Ex* m_Device;
|
||||
IDirect3DSurface9* m_RenderTarget;
|
||||
IDirectXVideoProcessorService* m_ProcService;
|
||||
@@ -63,7 +67,5 @@ private:
|
||||
DXVA2_ValueRange m_SaturationRange;
|
||||
DXVA2_VideoDesc m_Desc;
|
||||
REFERENCE_TIME m_FrameIndex;
|
||||
LPD3DXFONT m_DebugOverlayFont;
|
||||
LPD3DXFONT m_StatusOverlayFont;
|
||||
bool m_BlockingPresent;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
||||
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
||||
if (!EGLExtensionsStr) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions: %d", eglGetError());
|
||||
return QStringList();
|
||||
}
|
||||
return QString(EGLExtensionsStr).split(" ");
|
||||
|
||||
@@ -15,18 +15,6 @@
|
||||
#include <SDL_render.h>
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
#ifndef EGL_VERSION_1_5
|
||||
typedef intptr_t EGLAttrib;
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
#endif
|
||||
|
||||
#ifndef EGL_EXT_platform_base
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||
#endif
|
||||
|
||||
// These are EGL extensions, so some platform headers may not provide them
|
||||
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
||||
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
||||
@@ -34,6 +22,15 @@ typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platf
|
||||
#ifndef EGL_PLATFORM_X11_KHR
|
||||
#define EGL_PLATFORM_X11_KHR 0x31D5
|
||||
#endif
|
||||
#ifndef EGL_PLATFORM_GBM_KHR
|
||||
#define EGL_PLATFORM_GBM_KHR 0x31D7
|
||||
#endif
|
||||
|
||||
typedef struct _OVERLAY_VERTEX
|
||||
{
|
||||
float x, y;
|
||||
float u, v;
|
||||
} OVERLAY_VERTEX, *POVERLAY_VERTEX;
|
||||
|
||||
/* TODO:
|
||||
* - handle more pixel formats
|
||||
@@ -62,10 +59,14 @@ SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
|
||||
|
||||
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||
:
|
||||
m_SwPixelFormat(AV_PIX_FMT_NONE),
|
||||
m_EGLImagePixelFormat(AV_PIX_FMT_NONE),
|
||||
m_EGLDisplay(EGL_NO_DISPLAY),
|
||||
m_Textures{0},
|
||||
m_OverlayTextures{0},
|
||||
m_OverlayVbos{0},
|
||||
m_OverlayHasValidData{},
|
||||
m_ShaderProgram(0),
|
||||
m_OverlayShaderProgram(0),
|
||||
m_Context(0),
|
||||
m_Window(nullptr),
|
||||
m_Backend(backendRenderer),
|
||||
@@ -73,6 +74,7 @@ EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||
m_ColorSpace(AVCOL_SPC_NB),
|
||||
m_ColorFull(false),
|
||||
m_BlockingSwapBuffers(false),
|
||||
m_LastFrame(av_frame_alloc()),
|
||||
m_glEGLImageTargetTexture2DOES(nullptr),
|
||||
m_glGenVertexArraysOES(nullptr),
|
||||
m_glBindVertexArrayOES(nullptr),
|
||||
@@ -96,10 +98,26 @@ EGLRenderer::~EGLRenderer()
|
||||
if (m_ShaderProgram) {
|
||||
glDeleteProgram(m_ShaderProgram);
|
||||
}
|
||||
if (m_OverlayShaderProgram) {
|
||||
glDeleteProgram(m_OverlayShaderProgram);
|
||||
}
|
||||
if (m_VAO) {
|
||||
SDL_assert(m_glDeleteVertexArraysOES != nullptr);
|
||||
m_glDeleteVertexArraysOES(1, &m_VAO);
|
||||
}
|
||||
for (int i = 0; i < EGL_MAX_PLANES; i++) {
|
||||
if (m_Textures[i] != 0) {
|
||||
glDeleteTextures(1, &m_Textures[i]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
if (m_OverlayTextures[i] != 0) {
|
||||
glDeleteTextures(1, &m_OverlayTextures[i]);
|
||||
}
|
||||
if (m_OverlayVbos[i] != 0) {
|
||||
glDeleteBuffers(1, &m_OverlayVbos[i]);
|
||||
}
|
||||
}
|
||||
SDL_GL_DeleteContext(m_Context);
|
||||
}
|
||||
|
||||
@@ -107,6 +125,8 @@ EGLRenderer::~EGLRenderer()
|
||||
SDL_DestroyRenderer(m_DummyRenderer);
|
||||
}
|
||||
|
||||
av_frame_free(&m_LastFrame);
|
||||
|
||||
// Reset the global properties back to what they were before
|
||||
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
||||
@@ -123,21 +143,116 @@ bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
return true;
|
||||
}
|
||||
|
||||
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType)
|
||||
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||
{
|
||||
// TODO: FIXME
|
||||
// We handle uploading the updated overlay texture in renderOverlay().
|
||||
// notifyOverlayUpdated() is called on an arbitrary thread, which may
|
||||
// not be have the OpenGL context current on it.
|
||||
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
// If the overlay has been disabled, mark the data as invalid/stale.
|
||||
SDL_AtomicSet(&m_OverlayHasValidData[type], 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
|
||||
bool EGLRenderer::isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat)
|
||||
{
|
||||
// Remember to keep this in sync with EGLRenderer::renderFrame()!
|
||||
switch (pixelFormat)
|
||||
{
|
||||
case AV_PIX_FMT_NV12:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
// Pixel format support should be determined by the backend renderer
|
||||
return m_Backend->isPixelFormatSupported(videoFormat, pixelFormat);
|
||||
}
|
||||
|
||||
AVPixelFormat EGLRenderer::getPreferredPixelFormat(int videoFormat)
|
||||
{
|
||||
// Pixel format preference should be determined by the backend renderer
|
||||
return m_Backend->getPreferredPixelFormat(videoFormat);
|
||||
}
|
||||
|
||||
void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
{
|
||||
// Do nothing if this overlay is disabled
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload a new overlay texture if needed
|
||||
SDL_Surface* newSurface = Session::get()->getOverlayManager().getUpdatedOverlaySurface(type);
|
||||
if (newSurface != nullptr) {
|
||||
SDL_assert(!SDL_MUSTLOCK(newSurface));
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newSurface->w, newSurface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, newSurface->pixels);
|
||||
|
||||
// SDL_FRect wasn't added until 2.0.10
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} overlayRect = {};
|
||||
|
||||
// These overlay positions differ from the other renderers because OpenGL
|
||||
// places the origin in the lower-left corner instead of the upper-left.
|
||||
if (type == Overlay::OverlayStatusUpdate) {
|
||||
// Bottom Left
|
||||
overlayRect.x = 0;
|
||||
overlayRect.y = 0;
|
||||
}
|
||||
else if (type == Overlay::OverlayDebug) {
|
||||
// Top left
|
||||
overlayRect.x = 0;
|
||||
overlayRect.y = m_ViewportHeight - newSurface->h;
|
||||
} else {
|
||||
SDL_assert(false);
|
||||
}
|
||||
|
||||
overlayRect.w = newSurface->w;
|
||||
overlayRect.h = newSurface->h;
|
||||
|
||||
SDL_FreeSurface(newSurface);
|
||||
|
||||
// Convert screen space to normalized device coordinates
|
||||
overlayRect.x /= m_ViewportWidth / 2;
|
||||
overlayRect.w /= m_ViewportWidth / 2;
|
||||
overlayRect.y /= m_ViewportHeight / 2;
|
||||
overlayRect.h /= m_ViewportHeight / 2;
|
||||
overlayRect.x -= 1.0f;
|
||||
overlayRect.y -= 1.0f;
|
||||
|
||||
OVERLAY_VERTEX verts[] =
|
||||
{
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y + overlayRect.h, 1.0f, 0.0f},
|
||||
{overlayRect.x, overlayRect.y + overlayRect.h, 0.0f, 0.0f},
|
||||
{overlayRect.x, overlayRect.y, 0.0f, 1.0f},
|
||||
{overlayRect.x, overlayRect.y, 0.0f, 1.0f},
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y, 1.0f, 1.0f},
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y + overlayRect.h, 1.0f, 0.0f}
|
||||
};
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_OverlayVbos[type]);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||
|
||||
SDL_AtomicSet(&m_OverlayHasValidData[type], 1);
|
||||
}
|
||||
|
||||
if (!SDL_AtomicGet(&m_OverlayHasValidData[type])) {
|
||||
// If the overlay is not populated yet or is stale, don't render it.
|
||||
return;
|
||||
}
|
||||
|
||||
glUseProgram(m_OverlayShaderProgram);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_OverlayVbos[type]);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(OVERLAY_VERTEX), (void*)offsetof(OVERLAY_VERTEX, x));
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(OVERLAY_VERTEX), (void*)offsetof(OVERLAY_VERTEX, u));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||
glUniform1i(m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE], 0);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
int EGLRenderer::loadAndBuildShader(int shaderType,
|
||||
@@ -214,49 +329,85 @@ bool EGLRenderer::openDisplay(unsigned int platform, void* nativeDisplay)
|
||||
return m_EGLDisplay != EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
bool EGLRenderer::compileShader() {
|
||||
SDL_assert(!m_ShaderProgram);
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
unsigned EGLRenderer::compileShader(const char* vertexShaderSrc, const char* fragmentShaderSrc) {
|
||||
unsigned shader = 0;
|
||||
|
||||
// XXX: TODO: other formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
|
||||
bool ret = false;
|
||||
|
||||
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, "egl.vert");
|
||||
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, vertexShaderSrc);
|
||||
if (!vertexShader)
|
||||
return false;
|
||||
|
||||
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, "egl.frag");
|
||||
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, fragmentShaderSrc);
|
||||
if (!fragmentShader)
|
||||
goto fragError;
|
||||
|
||||
m_ShaderProgram = glCreateProgram();
|
||||
if (!m_ShaderProgram) {
|
||||
shader = glCreateProgram();
|
||||
if (!shader) {
|
||||
EGL_LOG(Error, "Cannot create shader program");
|
||||
goto progFailCreate;
|
||||
}
|
||||
|
||||
glAttachShader(m_ShaderProgram, vertexShader);
|
||||
glAttachShader(m_ShaderProgram, fragmentShader);
|
||||
glLinkProgram(m_ShaderProgram);
|
||||
glAttachShader(shader, vertexShader);
|
||||
glAttachShader(shader, fragmentShader);
|
||||
glLinkProgram(shader);
|
||||
int status;
|
||||
glGetProgramiv(m_ShaderProgram, GL_LINK_STATUS, &status);
|
||||
if (status) {
|
||||
ret = true;
|
||||
} else {
|
||||
glGetProgramiv(shader, GL_LINK_STATUS, &status);
|
||||
if (!status) {
|
||||
char shader_log[512];
|
||||
glGetProgramInfoLog(m_ShaderProgram, sizeof (shader_log), nullptr, shader_log);
|
||||
glGetProgramInfoLog(shader, sizeof (shader_log), nullptr, shader_log);
|
||||
EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
|
||||
glDeleteProgram(m_ShaderProgram);
|
||||
m_ShaderProgram = 0;
|
||||
glDeleteProgram(shader);
|
||||
shader = 0;
|
||||
}
|
||||
|
||||
progFailCreate:
|
||||
glDeleteShader(fragmentShader);
|
||||
fragError:
|
||||
glDeleteShader(vertexShader);
|
||||
return ret;
|
||||
return shader;
|
||||
}
|
||||
|
||||
bool EGLRenderer::compileShaders() {
|
||||
SDL_assert(!m_ShaderProgram);
|
||||
SDL_assert(!m_OverlayShaderProgram);
|
||||
|
||||
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
// XXX: TODO: other formats
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||
m_ShaderProgram = compileShader("egl_nv12.vert", "egl_nv12.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgramParams[NV12_PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||
m_ShaderProgramParams[NV12_PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||
m_ShaderProgramParams[NV12_PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||
m_ShaderProgramParams[NV12_PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||
}
|
||||
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||
m_ShaderProgram = compileShader("egl_opaque.vert", "egl_opaque.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE] = glGetUniformLocation(m_ShaderProgram, "uTexture");
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unsupported EGL pixel format: %d",
|
||||
m_EGLImagePixelFormat);
|
||||
SDL_assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag");
|
||||
if (!m_OverlayShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
@@ -339,6 +490,13 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||
case SDL_SYSWM_KMSDRM:
|
||||
if (!openDisplay(EGL_PLATFORM_GBM_KHR, info.info.kmsdrm.gbm_dev)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
EGL_LOG(Error, "not compatible with SYSWM");
|
||||
@@ -404,17 +562,30 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
src.x = src.y = dst.x = dst.y = 0;
|
||||
src.w = params->width;
|
||||
src.h = params->height;
|
||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||
SDL_GL_GetDrawableSize(m_Window, &dst.w, &dst.h);
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||
|
||||
m_ViewportWidth = dst.w;
|
||||
m_ViewportHeight = dst.h;
|
||||
|
||||
glClearColor(0, 0, 0, 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (params->enableVsync) {
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
m_BlockingSwapBuffers = true;
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||
// The SDL KMSDRM backend already enforces double buffering (due to
|
||||
// SDL_HINT_VIDEO_DOUBLE_BUFFER=1), so calling glFinish() after
|
||||
// SDL_GL_SwapWindow() will block an extra frame and lock rendering
|
||||
// at 1/2 the display refresh rate.
|
||||
if (info.subsystem != SDL_SYSWM_KMSDRM)
|
||||
#endif
|
||||
{
|
||||
m_BlockingSwapBuffers = true;
|
||||
}
|
||||
} else {
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
}
|
||||
@@ -430,6 +601,19 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
glGenBuffers(Overlay::OverlayMax, m_OverlayVbos);
|
||||
glGenTextures(Overlay::OverlayMax, m_OverlayTextures);
|
||||
for (size_t i = 0; i < Overlay::OverlayMax; ++i) {
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[i]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR)
|
||||
EGL_LOG(Error, "OpenGL error: %d", err);
|
||||
@@ -440,6 +624,13 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
return err == GL_NO_ERROR;
|
||||
}
|
||||
|
||||
const float *EGLRenderer::getColorOffsets() {
|
||||
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
|
||||
return m_ColorFull ? fullOffsets : limitedOffsets;
|
||||
}
|
||||
|
||||
const float *EGLRenderer::getColorMatrix() {
|
||||
/* The conversion matrices are shamelessly stolen from linux:
|
||||
* drivers/media/platform/imx-pxp.c:pxp_setup_csc
|
||||
@@ -478,18 +669,27 @@ const float *EGLRenderer::getColorMatrix() {
|
||||
switch (m_ColorSpace) {
|
||||
case AVCOL_SPC_SMPTE170M:
|
||||
case AVCOL_SPC_BT470BG:
|
||||
EGL_LOG(Info, "BT-601 pixels");
|
||||
return m_ColorFull ? bt601Full : bt601Lim;
|
||||
case AVCOL_SPC_BT709:
|
||||
EGL_LOG(Info, "BT-709 pixels");
|
||||
return m_ColorFull ? bt709Full : bt709Lim;
|
||||
case AVCOL_SPC_BT2020_NCL:
|
||||
case AVCOL_SPC_BT2020_CL:
|
||||
EGL_LOG(Info, "BT-2020 pixels");
|
||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||
default:
|
||||
// Some backends don't populate this, so we'll assume
|
||||
// the host gave us what we asked for by default.
|
||||
switch (getDecoderColorspace()) {
|
||||
case COLORSPACE_REC_601:
|
||||
return m_ColorFull ? bt601Full : bt601Lim;
|
||||
case COLORSPACE_REC_709:
|
||||
return m_ColorFull ? bt709Full : bt709Lim;
|
||||
case COLORSPACE_REC_2020:
|
||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
}
|
||||
};
|
||||
EGL_LOG(Warn, "unknown color space: %d, falling back to BT-601",
|
||||
m_ColorSpace);
|
||||
|
||||
return bt601Lim;
|
||||
}
|
||||
|
||||
@@ -499,7 +699,7 @@ bool EGLRenderer::specialize() {
|
||||
// Attach our GL context to the render thread
|
||||
SDL_GL_MakeCurrent(m_Window, m_Context);
|
||||
|
||||
if (!compileShader())
|
||||
if (!compileShaders())
|
||||
return false;
|
||||
|
||||
// The viewport should have the aspect ratio of the video stream
|
||||
@@ -539,20 +739,6 @@ bool EGLRenderer::specialize() {
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
m_glBindVertexArrayOES(0);
|
||||
|
||||
int yuvmatLocation = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||
glUniformMatrix3fv(yuvmatLocation, 1, GL_FALSE, getColorMatrix());
|
||||
|
||||
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
|
||||
int offLocation = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||
glUniform3fv(offLocation, 1, m_ColorFull ? fullOffsets : limitedOffsets);
|
||||
|
||||
int colorPlane = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||
glUniform1i(colorPlane, 0);
|
||||
colorPlane = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||
glUniform1i(colorPlane, 1);
|
||||
|
||||
glDeleteBuffers(1, &VBO);
|
||||
glDeleteBuffers(1, &EBO);
|
||||
|
||||
@@ -574,46 +760,58 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr) {
|
||||
// Find the native read-back format and load the shader
|
||||
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
|
||||
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||
// Find the native read-back format and load the shaders
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NONE) {
|
||||
m_EGLImagePixelFormat = m_Backend->getEGLImagePixelFormat();
|
||||
EGL_LOG(Info, "EGLImage pixel format: %d", m_EGLImagePixelFormat);
|
||||
|
||||
m_SwPixelFormat = hwFrameCtx->sw_format;
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
|
||||
m_ColorSpace = frame->colorspace;
|
||||
|
||||
// XXX: TODO: Handle other pixel formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
m_ColorSpace = frame->colorspace;
|
||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||
// This handles the case where the color range is unknown,
|
||||
// so that we use Limited color range which is the default
|
||||
// behavior for Moonlight.
|
||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||
|
||||
if (!specialize()) {
|
||||
m_SwPixelFormat = AV_PIX_FMT_NONE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count < 0)
|
||||
if (!specialize()) {
|
||||
m_EGLImagePixelFormat = AV_PIX_FMT_NONE;
|
||||
return;
|
||||
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||
}
|
||||
} else {
|
||||
// TODO: load texture for SW decoding ?
|
||||
EGL_LOG(Error, "EGL rendering only supports hw frames");
|
||||
}
|
||||
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count < 0)
|
||||
return;
|
||||
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||
}
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_ShaderProgram);
|
||||
m_glBindVertexArrayOES(m_VAO);
|
||||
|
||||
// Bind parameters for the shaders
|
||||
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||
glUniformMatrix3fv(m_ShaderProgramParams[NV12_PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
||||
glUniform3fv(m_ShaderProgramParams[NV12_PARAM_OFFSET], 1, getColorOffsets());
|
||||
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE1], 0);
|
||||
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE2], 1);
|
||||
}
|
||||
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||
glUniform1i(m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE], 0);
|
||||
}
|
||||
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
|
||||
m_glBindVertexArrayOES(0);
|
||||
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
renderOverlay((Overlay::OverlayType)i);
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(m_Window);
|
||||
|
||||
if (m_BlockingSwapBuffers) {
|
||||
@@ -628,6 +826,31 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
||||
glFinish();
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr)
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
|
||||
// Free the DMA-BUF backing the last frame now that it is definitely
|
||||
// no longer being used anymore. While the PRIME FD stays around until
|
||||
// EGL is done with it, the memory backing it may be reused by FFmpeg
|
||||
// before the GPU has read it. This is particularly noticeable on the
|
||||
// RK3288-based TinkerBoard when V-Sync is disabled.
|
||||
av_frame_unref(m_LastFrame);
|
||||
av_frame_move_ref(m_LastFrame, frame);
|
||||
}
|
||||
|
||||
bool EGLRenderer::testRenderFrame(AVFrame* frame)
|
||||
{
|
||||
EGLImage imgs[EGL_MAX_PLANES];
|
||||
|
||||
// Make sure we can get working EGLImages from the backend renderer.
|
||||
// Some devices (Raspberry Pi) will happily decode into DRM formats that
|
||||
// its own GL implementation won't accept in eglCreateImage().
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count <= 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Backend failed to export EGL image for test frame");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,21 +12,33 @@ public:
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual bool testRenderFrame(AVFrame* frame) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
|
||||
private:
|
||||
|
||||
bool compileShader();
|
||||
void renderOverlay(Overlay::OverlayType type);
|
||||
unsigned compileShader(const char* vertexShaderSrc, const char* fragmentShaderSrc);
|
||||
bool compileShaders();
|
||||
bool specialize();
|
||||
const float *getColorOffsets();
|
||||
const float *getColorMatrix();
|
||||
static int loadAndBuildShader(int shaderType, const char *filename);
|
||||
bool openDisplay(unsigned int platform, void* nativeDisplay);
|
||||
|
||||
int m_SwPixelFormat;
|
||||
int m_ViewportWidth;
|
||||
int m_ViewportHeight;
|
||||
|
||||
AVPixelFormat m_EGLImagePixelFormat;
|
||||
void *m_EGLDisplay;
|
||||
unsigned m_Textures[EGL_MAX_PLANES];
|
||||
unsigned m_OverlayTextures[Overlay::OverlayMax];
|
||||
unsigned m_OverlayVbos[Overlay::OverlayMax];
|
||||
SDL_atomic_t m_OverlayHasValidData[Overlay::OverlayMax];
|
||||
unsigned m_ShaderProgram;
|
||||
unsigned m_OverlayShaderProgram;
|
||||
SDL_GLContext m_Context;
|
||||
SDL_Window *m_Window;
|
||||
IFFmpegRenderer *m_Backend;
|
||||
@@ -34,11 +46,22 @@ private:
|
||||
int m_ColorSpace;
|
||||
bool m_ColorFull;
|
||||
bool m_BlockingSwapBuffers;
|
||||
AVFrame* m_LastFrame;
|
||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
||||
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
||||
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
||||
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
||||
|
||||
#define NV12_PARAM_YUVMAT 0
|
||||
#define NV12_PARAM_OFFSET 1
|
||||
#define NV12_PARAM_PLANE1 2
|
||||
#define NV12_PARAM_PLANE2 3
|
||||
#define OPAQUE_PARAM_TEXTURE 0
|
||||
int m_ShaderProgramParams[4];
|
||||
|
||||
#define OVERLAY_PARAM_TEXTURE 0
|
||||
int m_OverlayShaderProgramParams[1];
|
||||
|
||||
int m_OldContextProfileMask;
|
||||
int m_OldContextMajorVersion;
|
||||
int m_OldContextMinorVersion;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user