mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-14 20:44:19 +00:00
Compare commits
422
Commits
SDL3
...
linuxdeploy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fa83ea330 | ||
|
|
c21affd15a | ||
|
|
7de4e74829 | ||
|
|
9d6218d1ff | ||
|
|
8cc3b30642 | ||
|
|
d77e2cc913 | ||
|
|
453435f916 | ||
|
|
f222aa7985 | ||
|
|
5faa5fece8 | ||
|
|
51f86caac3 | ||
|
|
5d813ed3df | ||
|
|
d17575d47c | ||
|
|
0e50a9d457 | ||
|
|
ed029f194f | ||
|
|
569153220a | ||
|
|
e0367d05ae | ||
|
|
53e0c2534d | ||
|
|
a766c9cafc | ||
|
|
c685021f21 | ||
|
|
af03f57e08 | ||
|
|
1fc1122c65 | ||
|
|
94d47e95fe | ||
|
|
1cffa7b350 | ||
|
|
fae1cd3dee | ||
|
|
2160f029d2 | ||
|
|
1ec1a3d7ad | ||
|
|
eb3d8ad92c | ||
|
|
07b00a6987 | ||
|
|
4ec682dc72 | ||
|
|
a9a1ea0a0b | ||
|
|
c8d18f8a8e | ||
|
|
9c11dca454 | ||
|
|
9496c25271 | ||
|
|
6ff3309677 | ||
|
|
e4c211b987 | ||
|
|
cac3bb28d7 | ||
|
|
78bc2141f6 | ||
|
|
effac744c7 | ||
|
|
5676a076fd | ||
|
|
b6407492c7 | ||
|
|
91ce7a41e5 | ||
|
|
89c8462e9e | ||
|
|
53a7680a38 | ||
|
|
88b4a17fe1 | ||
|
|
8326223220 | ||
|
|
4aa5ad71c0 | ||
|
|
e785be03c2 | ||
|
|
db294fe4ae | ||
|
|
2e9fbecfea | ||
|
|
7c066485f3 | ||
|
|
11d51e121a | ||
|
|
09675bf56d | ||
|
|
8f994dd113 | ||
|
|
e596c2dcdc | ||
|
|
209511b2dc | ||
|
|
efa67fec1e | ||
|
|
be0aeb6cc2 | ||
|
|
8d5072533f | ||
|
|
4c14a24960 | ||
|
|
e3702dbab7 | ||
|
|
e4325b8736 | ||
|
|
eb294e63f4 | ||
|
|
9d597a666b | ||
|
|
33e0b08699 | ||
|
|
31898bbab4 | ||
|
|
34c9e01a96 | ||
|
|
ddc6675dc8 | ||
|
|
f0626e8cae | ||
|
|
6f4ced5ed6 | ||
|
|
0a2b134d49 | ||
|
|
f6a9e7694a | ||
|
|
8795fb54f4 | ||
|
|
092eb1b117 | ||
|
|
179857f17d | ||
|
|
d878447413 | ||
|
|
60fd840d7a | ||
|
|
5020fc6f48 | ||
|
|
a2122e17c6 | ||
|
|
6bb7c8bf2a | ||
|
|
2dda65078f | ||
|
|
2914ff678d | ||
|
|
a56efe5c36 | ||
|
|
b7a05d73b9 | ||
|
|
6d5b9e4706 | ||
|
|
9813932c1b | ||
|
|
d4d4ab5b06 | ||
|
|
402ac59390 | ||
|
|
c33c49e6fa | ||
|
|
e1bbf8144e | ||
|
|
f4712718cb | ||
|
|
d040bd24d1 | ||
|
|
6d40c61850 | ||
|
|
b4a5d21f97 | ||
|
|
792f762c15 | ||
|
|
8c9c799f81 | ||
|
|
4eaf037c75 | ||
|
|
b732b58807 | ||
|
|
d484ec3ac8 | ||
|
|
f5f06ae44e | ||
|
|
d2fa488979 | ||
|
|
cd9aceb62c | ||
|
|
d5b7ef0c58 | ||
|
|
e4be57db5d | ||
|
|
1b0c657864 | ||
|
|
64fea80ac9 | ||
|
|
990804fe6b | ||
|
|
05ef938e2c | ||
|
|
1e825c85e5 | ||
|
|
a0a4c1ea83 | ||
|
|
b5e7dec378 | ||
|
|
adbd19e6fa | ||
|
|
02a86167da | ||
|
|
a739c1cef8 | ||
|
|
f6e08f8a43 | ||
|
|
218ffc2d55 | ||
|
|
0f49dca4c0 | ||
|
|
30274f3ae8 | ||
|
|
c696ae7a7e | ||
|
|
66a30c66f3 | ||
|
|
198a208b44 | ||
|
|
21ce23e377 | ||
|
|
cd4ae1c0ed | ||
|
|
d865c77975 | ||
|
|
cd13910e6c | ||
|
|
b8e9262b1b | ||
|
|
c14ebb1ad7 | ||
|
|
d3957b3cbb | ||
|
|
a8e8efb17b | ||
|
|
75adc22d05 | ||
|
|
2549efc8b3 | ||
|
|
6f8e517ce1 | ||
|
|
09f8e7117b | ||
|
|
88719cc8bf | ||
|
|
3ea62c0800 | ||
|
|
7643cc929e | ||
|
|
a58d3cfe39 | ||
|
|
b41c4022ea | ||
|
|
788675ad59 | ||
|
|
be9f465008 | ||
|
|
8b6ad55c9b | ||
|
|
92b68a40d1 | ||
|
|
a931161923 | ||
|
|
37b9b6ed9e | ||
|
|
fad197fdce | ||
|
|
cda65ab58d | ||
|
|
9ffe5218d5 | ||
|
|
ed98f256e8 | ||
|
|
8aec35a650 | ||
|
|
a3237b741c | ||
|
|
377409f313 | ||
|
|
80ddd5ba13 | ||
|
|
4a547eb05e | ||
|
|
1c6ed8f390 | ||
|
|
0a58af4fa1 | ||
|
|
50a094c917 | ||
|
|
d14cf984f4 | ||
|
|
13d9d79fe3 | ||
|
|
70d4f24455 | ||
|
|
705cd3ddc4 | ||
|
|
ea5b062da3 | ||
|
|
e6f41f5574 | ||
|
|
9af6222039 | ||
|
|
476414ea71 | ||
|
|
83d770cdba | ||
|
|
25e2d39e26 | ||
|
|
bd32370b6d | ||
|
|
8726c1c7d0 | ||
|
|
34881599f5 | ||
|
|
de0fb72424 | ||
|
|
46c76534bc | ||
|
|
67cba9e397 | ||
|
|
2ab72375c6 | ||
|
|
d3118bd677 | ||
|
|
b8c3658c42 | ||
|
|
d3fb52c5a0 | ||
|
|
7924a5b6a7 | ||
|
|
2972022ced | ||
|
|
c0846155c3 | ||
|
|
ac6dadffc6 | ||
|
|
745ac34b15 | ||
|
|
5f89636ed7 | ||
|
|
46d58e55a3 | ||
|
|
be460970a8 | ||
|
|
d50ba06321 | ||
|
|
b108684edc | ||
|
|
b6d10a34c8 | ||
|
|
232ff0b3c6 | ||
|
|
c5960c3e15 | ||
|
|
b48cbd76f5 | ||
|
|
ceef792f04 | ||
|
|
f3f1d56e8d | ||
|
|
39e24e9c8c | ||
|
|
c26e437f40 | ||
|
|
e89f63b8fb | ||
|
|
cf8251b9e6 | ||
|
|
d6c9beffc1 | ||
|
|
93dc6d6b60 | ||
|
|
8ee82421b3 | ||
|
|
31c65e0863 | ||
|
|
3e5aa9b127 | ||
|
|
085480e9f4 | ||
|
|
c3ce0918b3 | ||
|
|
c5b7a9c679 | ||
|
|
f1451a0db3 | ||
|
|
8a5f34fb01 | ||
|
|
d1f43ca258 | ||
|
|
249c29b74c | ||
|
|
1cf207de07 | ||
|
|
4f08d52754 | ||
|
|
41ad3c0938 | ||
|
|
21fb2832e1 | ||
|
|
f0bb1031d8 | ||
|
|
b80d2a00c0 | ||
|
|
85ea28287e | ||
|
|
a6f8901a18 | ||
|
|
d501a627f0 | ||
|
|
526d815930 | ||
|
|
b448e5bbd7 | ||
|
|
741e360024 | ||
|
|
7edbede95d | ||
|
|
7f54662160 | ||
|
|
f66285ddba | ||
|
|
bf250a4edb | ||
|
|
bdfadb1cdc | ||
|
|
63a381a882 | ||
|
|
4a591069ac | ||
|
|
c71b513bc4 | ||
|
|
700655c051 | ||
|
|
3425fec33d | ||
|
|
de844f2a0c | ||
|
|
54163e30d0 | ||
|
|
bd6a1d9a9b | ||
|
|
b25801ff7d | ||
|
|
bc5e9917cd | ||
|
|
d8eb817ed9 | ||
|
|
c1e5b0bf66 | ||
|
|
26f96a20a9 | ||
|
|
dbcc6a9089 | ||
|
|
9c6d2bab19 | ||
|
|
c92dc2495e | ||
|
|
1c24aada0a | ||
|
|
58f9b2a83b | ||
|
|
5feb331c46 | ||
|
|
9d5f25f828 | ||
|
|
d3c23b55dc | ||
|
|
80b7a69cd2 | ||
|
|
296387345d | ||
|
|
42bf87753f | ||
|
|
d262e5d1f6 | ||
|
|
f0821d9543 | ||
|
|
4164a222ae | ||
|
|
245cd7f1a8 | ||
|
|
62816a6b2a | ||
|
|
f5ef201905 | ||
|
|
01288be937 | ||
|
|
f57257cbfd | ||
|
|
d40999ce06 | ||
|
|
cb6d770b92 | ||
|
|
4d6196c3d3 | ||
|
|
54283ce0a1 | ||
|
|
6313129603 | ||
|
|
84d7a22638 | ||
|
|
f0aff0b0fb | ||
|
|
be358e9503 | ||
|
|
379d5ca376 | ||
|
|
8ed7144751 | ||
|
|
66b96e75af | ||
|
|
1160f40655 | ||
|
|
d5abf568c6 | ||
|
|
8324f61db0 | ||
|
|
9106f2c420 | ||
|
|
a2670dd568 | ||
|
|
c54e6e4459 | ||
|
|
dd6f1c5873 | ||
|
|
7fab5007a8 | ||
|
|
7f7cc89e61 | ||
|
|
7116efd8de | ||
|
|
229f5e4cea | ||
|
|
af37002a60 | ||
|
|
181dba5864 | ||
|
|
fd314db27e | ||
|
|
f67272b153 | ||
|
|
9b3050514b | ||
|
|
2c12ad297f | ||
|
|
50223dbdb8 | ||
|
|
beadf7819b | ||
|
|
7d829ac0c4 | ||
|
|
2f6e7608f8 | ||
|
|
5244313b30 | ||
|
|
c13e6b08c1 | ||
|
|
151f5dd678 | ||
|
|
a34d40e3ee | ||
|
|
68aa04926e | ||
|
|
7d544c1ce4 | ||
|
|
ac7696ea8f | ||
|
|
e79bf664dd | ||
|
|
4d303cebee | ||
|
|
bd6235efba | ||
|
|
b1232e0ed4 | ||
|
|
c35f7086a0 | ||
|
|
c7bc76325f | ||
|
|
ccaca68570 | ||
|
|
57db20016a | ||
|
|
bdb6d02dac | ||
|
|
9bcc6291be | ||
|
|
4ec549650d | ||
|
|
14027f3c74 | ||
|
|
fb9a164111 | ||
|
|
200cab9d17 | ||
|
|
c52a57f0ec | ||
|
|
b4dc7ca7cb | ||
|
|
00c1dd0d0d | ||
|
|
ecfc24c5d4 | ||
|
|
e6e91ca88b | ||
|
|
260a0e0ae2 | ||
|
|
3f8f4744c5 | ||
|
|
4bbd02fb2d | ||
|
|
c5ca672865 | ||
|
|
4688f3650c | ||
|
|
2550f416f4 | ||
|
|
579ad25a01 | ||
|
|
1144dbccb3 | ||
|
|
ff81f74391 | ||
|
|
c0d38ee78f | ||
|
|
c9cb64f90b | ||
|
|
93e597a93f | ||
|
|
82b33c033a | ||
|
|
be266d3349 | ||
|
|
c618a0b5df | ||
|
|
a20d429bc1 | ||
|
|
2b3e0803de | ||
|
|
4189903233 | ||
|
|
1a0dd56bb9 | ||
|
|
3aa3bfa403 | ||
|
|
aa9a716bd5 | ||
|
|
6fd2b73e6b | ||
|
|
06b5c4631f | ||
|
|
997c4aa0ae | ||
|
|
f4343c5f29 | ||
|
|
3cdea6b039 | ||
|
|
ac2e10c712 | ||
|
|
61fa45ad21 | ||
|
|
f362e76127 | ||
|
|
749c69fc76 | ||
|
|
5dca30def1 | ||
|
|
502783a86b | ||
|
|
45989fdd6e | ||
|
|
490aa5082f | ||
|
|
ae1c65805c | ||
|
|
9cb4105aec | ||
|
|
1fd545ae1f | ||
|
|
3194cb09d8 | ||
|
|
f9bb45579b | ||
|
|
f7b2edc8e3 | ||
|
|
c9a3946d80 | ||
|
|
0c8354336b | ||
|
|
1bf86f52d3 | ||
|
|
504d42865d | ||
|
|
65c04fd560 | ||
|
|
11dc244857 | ||
|
|
b9cab4cac5 | ||
|
|
65647a32f4 | ||
|
|
777502b495 | ||
|
|
ce4309694f | ||
|
|
ccc64a5629 | ||
|
|
25f6c020b6 | ||
|
|
6a95742a92 | ||
|
|
1dbdcb5279 | ||
|
|
9ce0537587 | ||
|
|
76d03f015d | ||
|
|
edbc24fa3e | ||
|
|
d4fd50b973 | ||
|
|
d5ab795b36 | ||
|
|
c5fc5220f3 | ||
|
|
28dea31533 | ||
|
|
8ce6a9585f | ||
|
|
47452371db | ||
|
|
4001b05fca | ||
|
|
75e917622e | ||
|
|
75359bb1c4 | ||
|
|
c93f7e7385 | ||
|
|
48bcd725c9 | ||
|
|
e807a52cfa | ||
|
|
9c9bfd8428 | ||
|
|
29b1304337 | ||
|
|
cc0b574bb1 | ||
|
|
7a769172a1 | ||
|
|
d9c7a245ef | ||
|
|
469a5c78f1 | ||
|
|
fabb4fdadc | ||
|
|
9491884cb4 | ||
|
|
bf51577787 | ||
|
|
4c5bcee8dc | ||
|
|
fd70865026 | ||
|
|
351aaa6759 | ||
|
|
59bc625cc7 | ||
|
|
b2f765e8ef | ||
|
|
bdd9a3a994 | ||
|
|
2ecafabcab | ||
|
|
9ff2ac0974 | ||
|
|
5760d08c33 | ||
|
|
ff7e61c6d9 | ||
|
|
6ad96fba42 | ||
|
|
85856114b2 | ||
|
|
7decfae792 | ||
|
|
b6008b15dc | ||
|
|
260ec3d80d | ||
|
|
7ec2e50334 | ||
|
|
e532b9167a | ||
|
|
52d5890372 | ||
|
|
f34d11994f | ||
|
|
359c92340d | ||
|
|
f1d0e97681 | ||
|
|
208d048358 | ||
|
|
e571d5833c | ||
|
|
286676e5c1 | ||
|
|
6d47287b60 | ||
|
|
60fb6881b2 | ||
|
|
b79c116bd9 | ||
|
|
895d0a6bf3 | ||
|
|
ae2693a860 | ||
|
|
0783b28ba6 |
@@ -0,0 +1,9 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: Build - Linux AppImage
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ci_version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
CI_VERSION: ${{ inputs.ci_version }}
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
|
||||
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-1.4.313-jammy.list https://packages.lunarg.com/vulkan/1.4.313/lunarg-vulkan-1.4.313-jammy.list
|
||||
sudo apt update
|
||||
sudo apt install -y qt6-base-dev qt6-declarative-dev libqt6svg6-dev qt6-wayland qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts \
|
||||
qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick python3-pip nasm libgbm-dev libdrm-dev libfreetype-dev libasound2-dev \
|
||||
libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev \
|
||||
libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev libxcb-dri3-dev libx11-xcb-dev \
|
||||
libxfixes-dev libxtst-dev wayland-protocols libopus-dev libvdpau-dev libgl-dev libpipewire-0.3-dev liburing-dev vulkan-sdk cmake
|
||||
sudo pip3 install meson
|
||||
mkdir -p dep_root/{bin,include,lib}
|
||||
echo "DEP_ROOT=$PWD/dep_root" >> "${GITHUB_ENV}"
|
||||
echo "$PWD/dep_root/bin" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Checkout SDL3
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: libsdl-org/SDL
|
||||
ref: release-3.4.8
|
||||
path: deps/SDL
|
||||
|
||||
- name: Build SDL3
|
||||
working-directory: deps/SDL
|
||||
run: |
|
||||
cmake -DSDL_TEST_LIBRARY=OFF -DSDL_INSTALL_DOCS=OFF -S . -B build
|
||||
cmake --build build -j
|
||||
sudo cmake --install build
|
||||
|
||||
- name: Checkout sdl2-compat
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: libsdl-org/sdl2-compat
|
||||
ref: release-2.32.68
|
||||
path: deps/sdl2-compat
|
||||
|
||||
- name: Build sdl2-compat
|
||||
working-directory: deps/sdl2-compat
|
||||
run: |
|
||||
cmake -DSDL2COMPAT_TESTS=OFF -S . -B build
|
||||
cmake --build build -j
|
||||
sudo cmake --install build
|
||||
|
||||
- name: Checkout SDL_ttf
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: libsdl-org/SDL_ttf
|
||||
ref: a883e490e30fb44a5336ea3dcb990c6982c5216f
|
||||
path: deps/SDL_ttf
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Build SDL_ttf
|
||||
working-directory: deps/SDL_ttf
|
||||
run: |
|
||||
./autogen.sh
|
||||
./configure
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Checkout libva
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: intel/libva
|
||||
ref: 2.23.0
|
||||
path: deps/libva
|
||||
|
||||
- name: Build libva
|
||||
working-directory: deps/libva
|
||||
run: |
|
||||
./autogen.sh
|
||||
./configure --enable-x11
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Checkout libplacebo
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: haasn/libplacebo
|
||||
ref: v7.360.1
|
||||
path: deps/libplacebo
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Build libplacebo
|
||||
working-directory: deps/libplacebo
|
||||
run: |
|
||||
meson setup build -Dvulkan=enabled -Dopengl=disabled -Ddemos=false
|
||||
ninja -C build
|
||||
sudo ninja install -C build
|
||||
|
||||
- name: Build dav1d
|
||||
working-directory: deps
|
||||
env:
|
||||
DAV1D_VER: 1.5.3
|
||||
run: |
|
||||
git clone --branch $DAV1D_VER --depth 1 https://code.videolan.org/videolan/dav1d.git
|
||||
pushd dav1d
|
||||
meson setup build -Ddefault_library=static -Dbuildtype=release -Denable_tools=false -Denable_tests=false
|
||||
ninja -C build
|
||||
sudo ninja install -C build
|
||||
popd
|
||||
|
||||
- name: Checkout FFmpeg
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: FFmpeg/FFmpeg
|
||||
ref: n8.1.1
|
||||
path: deps/FFmpeg
|
||||
|
||||
- name: Build FFmpeg
|
||||
working-directory: deps/FFmpeg
|
||||
run: |
|
||||
./configure --enable-pic --disable-static --enable-shared --disable-all --disable-autodetect --enable-avcodec --enable-avformat --enable-swscale \
|
||||
--enable-decoder=h264 --enable-decoder=hevc --enable-decoder=av1 \
|
||||
--enable-vaapi --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=av1_vaapi \
|
||||
--enable-vdpau --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau --enable-hwaccel=av1_vdpau \
|
||||
--enable-libdrm --enable-vulkan --enable-hwaccel=h264_vulkan --enable-hwaccel=hevc_vulkan --enable-hwaccel=av1_vulkan \
|
||||
--enable-libdav1d --enable-decoder=libdav1d
|
||||
make -j$(nproc)
|
||||
sudo make install
|
||||
|
||||
- name: Install linuxdeploy
|
||||
working-directory: dep_root/bin
|
||||
run: |
|
||||
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
chmod +x linuxdeploy*.AppImage
|
||||
|
||||
- name: Build Binaries
|
||||
run: |
|
||||
sudo ldconfig
|
||||
scripts/build-appimage.sh
|
||||
|
||||
- name: Upload Binaries
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Moonlight-LinuxAppImage-${{ env.CI_VERSION }}
|
||||
path: build/installer-release/Moonlight-${{ env.CI_VERSION }}-x86_64.AppImage
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: Build - Steam Link
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ci_version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
CI_VERSION: ${{ inputs.ci_version }}
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Checkout Steam Link SDK
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ValveSoftware/steamlink-sdk
|
||||
path: steamlink-sdk
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Build binaries
|
||||
run: STEAMLINK_SDK_PATH=$PWD/steamlink-sdk scripts/build-steamlink-app.sh
|
||||
|
||||
- name: Upload Binaries
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Moonlight-SteamLink-${{ env.CI_VERSION }}
|
||||
path: build/deploy-release/*
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: Build - Windows and macOS
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ci_version:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
env:
|
||||
CI_VERSION: ${{ inputs.ci_version }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Windows
|
||||
- os: windows-2025
|
||||
qt_version: 6.11.0
|
||||
# macOS
|
||||
- os: macos-15
|
||||
qt_version: 6.11.0
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Download dependencies (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: powershell ./setup-deps.ps1
|
||||
|
||||
- name: Install create-dmg (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install node
|
||||
npm install --global create-dmg
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
cache: true
|
||||
set-env: ${{ runner.os != 'Windows' }}
|
||||
add-tools-to-path: ${{ runner.os != 'Windows' }}
|
||||
version: ${{ matrix.qt_version }}
|
||||
aqtsource: 'git+https://github.com/miurahr/aqtinstall.git@073e34d7c2ab4ae6961ed7cca690b3abd5ba5a7e'
|
||||
|
||||
- name: Install Qt ARM64 (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
cache: true
|
||||
set-env: false
|
||||
add-tools-to-path: false
|
||||
arch: win64_msvc2022_arm64_cross_compiled
|
||||
version: ${{ matrix.qt_version }}
|
||||
|
||||
- name: Build x64 binaries (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: cmd
|
||||
run: |
|
||||
set "PATH=%QT_PATH%;%PATH%"
|
||||
scripts\build-arch.bat Release x64
|
||||
env:
|
||||
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_64\bin
|
||||
|
||||
- name: Build ARM64 binaries (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: cmd
|
||||
run: |
|
||||
set "PATH=%QT_PATH%;%PATH%"
|
||||
scripts\build-arch.bat Release arm64
|
||||
env:
|
||||
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_arm64\bin
|
||||
|
||||
- name: Build Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ($Env:RUNNER_OS -eq "Windows") {
|
||||
cmd /c scripts\generate-bundle.bat Release
|
||||
} else {
|
||||
bash scripts/generate-dmg.sh Release
|
||||
}
|
||||
|
||||
- name: Upload Binaries (Windows x64)
|
||||
uses: actions/upload-artifact@v6
|
||||
if: runner.os == 'Windows'
|
||||
with:
|
||||
name: Moonlight-${{ runner.os }}-x64-${{ env.CI_VERSION }}
|
||||
path: build/deploy-x64-release/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Binaries (Windows arm64)
|
||||
uses: actions/upload-artifact@v6
|
||||
if: runner.os == 'Windows'
|
||||
with:
|
||||
name: Moonlight-${{ runner.os }}-arm64-${{ env.CI_VERSION }}
|
||||
path: build/deploy-arm64-release/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Binaries (macOS)
|
||||
uses: actions/upload-artifact@v6
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
name: Moonlight-${{ runner.os }}-${{ env.CI_VERSION }}
|
||||
path: build/installer-Release/*.dmg
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Symbols
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: zDevDbgSyms-${{ runner.os }}-${{ env.CI_VERSION }}
|
||||
path: |
|
||||
build/installer-*/*.dsym/*
|
||||
build/symbols-*/*.pdb
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: Build
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ci_version: ${{ steps.set-ci-version.outputs.ci_version }}
|
||||
steps:
|
||||
- name: Set CI version
|
||||
id: set-ci-version
|
||||
shell: bash
|
||||
run: |
|
||||
CI_VERSION=`echo ${GITHUB_SHA} | cut -c1-6`
|
||||
echo "ci_version=${CI_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
build-appimage:
|
||||
needs: setup
|
||||
uses: ./.github/workflows/build-appimage.yml
|
||||
with:
|
||||
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||
|
||||
build-steamlink:
|
||||
needs: setup
|
||||
uses: ./.github/workflows/build-steamlink.yml
|
||||
with:
|
||||
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||
|
||||
build-windows-macos:
|
||||
needs: setup
|
||||
uses: ./.github/workflows/build-win-mac.yml
|
||||
with:
|
||||
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
|
||||
**/.vs/
|
||||
.vscode/
|
||||
build/
|
||||
libs/
|
||||
config.tests/*/.qmake.stash
|
||||
config.tests/*/Makefile
|
||||
|
||||
@@ -7,13 +7,6 @@
|
||||
[submodule "app/SDL_GameControllerDB"]
|
||||
path = app/SDL_GameControllerDB
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
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
|
||||
|
||||
@@ -207,6 +207,28 @@ private:
|
||||
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
||||
L"RTSSHooks.dll",
|
||||
L"RTSSHooks64.dll",
|
||||
|
||||
// Bandicam's Vulkan layer DLL crashes during destruction of the Vulkan swapchain
|
||||
// bdcamvk64+0x10b73:
|
||||
// 00007ffa`be3b0b73 498b5630 mov rdx,qword ptr [r14+30h] ds:00000000`00000030=????????????????
|
||||
//
|
||||
// bdcamvk64+0x10b73
|
||||
// libplacebo_357!vk_sw_destroy+0x14f [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\vulkan\swapchain.c @ 460]
|
||||
// libplacebo_357!pl_swapchain_destroy+0x1a [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\swapchain.c @ 30]
|
||||
// Moonlight!PlVkRenderer::~PlVkRenderer+0x159 [D:\a\moonlight-qt\app\streaming\video\ffmpeg-renderers\plvk.cpp @ 140]
|
||||
// Moonlight!PlVkRenderer::`scalar deleting destructor'+0x17
|
||||
// Moonlight!FFmpegVideoDecoder::reset+0x203 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 301]
|
||||
// Moonlight!FFmpegVideoDecoder::~FFmpegVideoDecoder+0x22 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 257]
|
||||
// Moonlight!FFmpegVideoDecoder::`scalar deleting destructor'+0x17
|
||||
// Moonlight!Session::getDecoderInfo+0x19a [D:\a\moonlight-qt\app\streaming\session.cpp @ 415]
|
||||
// Moonlight!SystemProperties::querySdlVideoInfoInternal+0x134 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 161]
|
||||
// Moonlight!SystemProperties::querySdlVideoInfo+0x97 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 123]
|
||||
// Moonlight!SystemProperties::SystemProperties+0x4bf [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 75]
|
||||
//
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/1425
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/1579
|
||||
L"bdcamvk32.dll",
|
||||
L"bdcamvk64.dll",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Moonlight also has mobile versions for [Android](https://github.com/moonlight-st
|
||||
|
||||
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/actions/workflows/build.yml?query=branch%3Amaster)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||
|
||||
@@ -31,6 +31,9 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- [Experimental RISC-V Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers)
|
||||
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||
|
||||
### Nightly Builds
|
||||
- [Downloads](https://nightly.link/moonlight-stream/moonlight-qt/workflows/build/master)
|
||||
|
||||
#### Special Thanks
|
||||
|
||||
[](https://cloudsmith.com)
|
||||
@@ -54,13 +57,13 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 6 is recommended, but Qt 5.9 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||
* Qt 6 is recommended, but Qt 5.12 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||
* GCC or Clang
|
||||
* FFmpeg 4.0 or later
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu:
|
||||
* Base Requirements: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libavformat-dev libswscale-dev libva-dev libvdpau-dev libxkbcommon-dev wayland-protocols libdrm-dev`
|
||||
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick`
|
||||
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev qt6-wayland qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick`
|
||||
* Qt 5: `qtbase5-dev qt5-qmake qtdeclarative5-dev qtquickcontrols2-5-dev qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2 qtwayland5`
|
||||
* RedHat/Fedora (RPM Fusion repo required):
|
||||
* Base Requirements: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel libdrm-devel`
|
||||
@@ -72,18 +75,33 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
|
||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||
|
||||
**Steam Link Hardware Limitations**
|
||||
Moonlight builds for Steam Link are subject to hardware limitations of the Steam Link device:
|
||||
* Maximum resolution: **1080p (1920x1080)**
|
||||
* Maximum framerate: **60 FPS**
|
||||
* Maximum video bitrate: **40 Mbps**
|
||||
* **HDR streaming is not supported** on the original hardware
|
||||
|
||||
### Docker containers
|
||||
If you want to use Docker for building, look at [this repo](https://github.com/cgutman/moonlight-packaging) containing canonical containers
|
||||
for different architectures, which handle building deps and extra linking for you.
|
||||
|
||||
### Build Setup Steps
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.12 or later.
|
||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
2. Download submodules and dependencies
|
||||
* Run `git submodule update --init --recursive` from within `moonlight-qt/`.
|
||||
* On Windows and macOS, you must also run `setup-deps.ps1` (Windows) or `setup-deps.py` (macOS).
|
||||
* Perform these steps each time you pull new changes from the Git repository.
|
||||
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\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on 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 on macOS or Linux, run `qmake6 moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To build from the command line for development use on macOS or Linux, run `qmake6 moonlight-qt.pro` then `make debug` or `make release`.
|
||||
* The final binary will be placed in `app/moonlight`.
|
||||
* To create an embedded build for a single-purpose device, use `qmake6 "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.
|
||||
* For platforms with poor GPU performance, add `"CONFIG+=gpuslow"` to prefer direct KMSDRM rendering over GL/Vulkan renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>GCSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Moonlight</string>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Stream games and other applications from another PC</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
@@ -44,5 +48,7 @@
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: e5a5fa2ac6...1fc89ca578
+29
-45
@@ -1,5 +1,5 @@
|
||||
QT += core quick network quickcontrols2 svg
|
||||
CONFIG += c++11
|
||||
CONFIG += c++17
|
||||
|
||||
unix:!macx {
|
||||
TARGET = moonlight
|
||||
@@ -36,35 +36,29 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x86
|
||||
!exists($$PWD/../libs/windows) {
|
||||
error("Missing dependencies. Please run 'powershell .\setup-deps.ps1' to fetch prebuilt libraries.")
|
||||
}
|
||||
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x64 $$PWD/../libs/windows/include/x64/SDL2
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/arm64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/arm64 $$PWD/../libs/windows/include/arm64/SDL2
|
||||
}
|
||||
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||
|
||||
# Work around a conflict with math.h inclusion between SDL and Qt 6
|
||||
DEFINES += _USE_MATH_DEFINES
|
||||
}
|
||||
macx:!disable-prebuilts {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
!exists($$PWD/../libs/mac) {
|
||||
error("Missing dependencies. Please run 'python3 setup-deps.py' to fetch prebuilt libraries.")
|
||||
}
|
||||
|
||||
# QMake doesn't handle framework-style includes correctly on its own
|
||||
QMAKE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_CXXFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/include/SDL2
|
||||
LIBS += -L$$PWD/../libs/mac/lib
|
||||
}
|
||||
|
||||
unix:if(!macx|disable-prebuilts) {
|
||||
@@ -122,7 +116,9 @@ unix:if(!macx|disable-prebuilts) {
|
||||
}
|
||||
}
|
||||
|
||||
!disable-cuda {
|
||||
# Disabled by default due to reliability issues. See #1314.
|
||||
# CUDA interop is superseded by VDPAU and Vulkan Video.
|
||||
enable-cuda {
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
@@ -157,20 +153,16 @@ win32 {
|
||||
CONFIG += ffmpeg libplacebo
|
||||
}
|
||||
win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
CONFIG += discord-rpc
|
||||
}
|
||||
macx {
|
||||
!disable-prebuilts {
|
||||
LIBS += -lssl.3 -lcrypto.3 -lavcodec.61 -lavutil.59 -lswscale.8 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lssl.3 -lcrypto.3 -lavcodec.62 -lavutil.60 -lswscale.9 -lopus -lSDL2 -lSDL2_ttf
|
||||
CONFIG += discord-rpc
|
||||
}
|
||||
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
|
||||
|
||||
# For libsoundio
|
||||
LIBS += -framework CoreAudio -framework AudioUnit
|
||||
|
||||
CONFIG += ffmpeg soundio
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
@@ -204,6 +196,7 @@ SOURCES += \
|
||||
streaming/audio/renderers/sdlaud.cpp \
|
||||
gui/computermodel.cpp \
|
||||
gui/appmodel.cpp \
|
||||
streaming/bandwidth.cpp \
|
||||
streaming/streamutils.cpp \
|
||||
backend/autoupdatechecker.cpp \
|
||||
path.cpp \
|
||||
@@ -241,6 +234,7 @@ HEADERS += \
|
||||
gui/computermodel.h \
|
||||
gui/appmodel.h \
|
||||
streaming/video/decoder.h \
|
||||
streaming/bandwidth.h \
|
||||
streaming/streamutils.h \
|
||||
backend/autoupdatechecker.h \
|
||||
path.h \
|
||||
@@ -329,9 +323,12 @@ libdrm {
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl
|
||||
!disable-masterhooks {
|
||||
message(Master hooks enabled)
|
||||
DEFINES += HAVE_DRM_MASTER_HOOKS
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl -pthread
|
||||
}
|
||||
}
|
||||
}
|
||||
cuda {
|
||||
@@ -414,13 +411,6 @@ macx {
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/vt.h
|
||||
}
|
||||
soundio {
|
||||
message(libsoundio audio renderer selected)
|
||||
|
||||
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
|
||||
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
||||
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
||||
}
|
||||
discord-rpc {
|
||||
message(Discord integration enabled)
|
||||
|
||||
@@ -486,7 +476,10 @@ TRANSLATIONS += \
|
||||
languages/qml_he.ts \
|
||||
languages/qml_ckb.ts \
|
||||
languages/qml_lt.ts \
|
||||
languages/qml_et.ts
|
||||
languages/qml_et.ts \
|
||||
languages/qml_bg.ts \
|
||||
languages/qml_eo.ts \
|
||||
languages/qml_ta.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
@@ -508,15 +501,6 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
|
||||
soundio {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||
|
||||
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||
}
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||
|
||||
@@ -58,7 +58,7 @@ void AutoUpdateChecker::start()
|
||||
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
||||
{
|
||||
QStringList list = string.split('.');
|
||||
for (const QString& component : list) {
|
||||
for (const QString& component : std::as_const(list)) {
|
||||
version.append(component.toInt());
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
return;
|
||||
}
|
||||
|
||||
for (QJsonValueRef updateEntry : array) {
|
||||
for (const auto& updateEntry : std::as_const(array)) {
|
||||
if (updateEntry.isObject()) {
|
||||
QJsonObject updateObj = updateEntry.toObject();
|
||||
if (!updateObj.contains("platform") ||
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <random>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||
@@ -30,9 +29,9 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(NvAddress address, bool& changed)
|
||||
bool tryPollComputer(QNetworkAccessManager* nam, NvAddress address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address, 0, m_Computer->serverCert);
|
||||
NvHTTP http(address, 0, m_Computer->serverCert, nam);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
@@ -53,9 +52,9 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
bool updateAppList(QNetworkAccessManager* nam, bool& changed)
|
||||
{
|
||||
NvHTTP http(m_Computer);
|
||||
NvHTTP http(m_Computer, nam);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
@@ -75,6 +74,21 @@ private:
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Reduce the power and performance impact of our
|
||||
// computer status polling while it's running.
|
||||
setPriority(QThread::LowPriority);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
setServiceLevel(QThread::QualityOfService::Eco);
|
||||
#endif
|
||||
|
||||
// Share the QNetworkAccessManager to conserve resources when polling.
|
||||
// Each instance creates a worker thread, so sharing them ensures that
|
||||
// we are not spamming a new thread for every single polling attempt.
|
||||
//
|
||||
// Since QThread inherit the priority of the current thread, this also
|
||||
// ensures that the NAM's worker thread will inherit our lower priority.
|
||||
QNetworkAccessManager nam;
|
||||
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
@@ -87,7 +101,7 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
if (tryPollComputer(&nam, address, stateChanged)) {
|
||||
if (!wasOnline) {
|
||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString();
|
||||
}
|
||||
@@ -118,7 +132,7 @@ private:
|
||||
stateChanged = false;
|
||||
}
|
||||
|
||||
if (updateAppList(stateChanged)) {
|
||||
if (updateAppList(&nam, stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
@@ -217,17 +231,17 @@ ComputerManager::~ComputerManager()
|
||||
m_MdnsBrowser = nullptr;
|
||||
|
||||
// Interrupt polling
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||
entry->interrupt();
|
||||
}
|
||||
|
||||
// Delete all polling entries (and associated threads)
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||
delete entry;
|
||||
}
|
||||
|
||||
// Destroy all NvComputer objects now that polling is halted
|
||||
for (NvComputer* computer : m_KnownHosts) {
|
||||
for (NvComputer* computer : std::as_const(m_KnownHosts)) {
|
||||
delete computer;
|
||||
}
|
||||
}
|
||||
@@ -254,7 +268,7 @@ void DelayedFlushThread::run() {
|
||||
|
||||
// Update the last serialized hosts map under the delayed flush mutex
|
||||
m_ComputerManager->m_LastSerializedHosts.clear();
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||
// Copy the current state of the NvComputer to allow us to check later if we need
|
||||
// to serialize it again when attribute updates occur.
|
||||
QReadLocker computerLock(&computer->lock);
|
||||
@@ -271,7 +285,7 @@ void DelayedFlushThread::run() {
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings, false);
|
||||
}
|
||||
@@ -284,7 +298,7 @@ void DelayedFlushThread::run() {
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings, true);
|
||||
}
|
||||
@@ -410,13 +424,15 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
bool added = false;
|
||||
|
||||
// Add the host using the IPv4 address
|
||||
for (const QHostAddress& address : addresses) {
|
||||
for (const QHostAddress& address : std::as_const(addresses)) {
|
||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
// NB: We don't just call addNewHost() here with v6Global because the IPv6
|
||||
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
|
||||
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
|
||||
// it's not currently reachable.
|
||||
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
||||
addNewHost(NvAddress(address, computer->port()),
|
||||
true, computer->hostname(),
|
||||
NvAddress(v6Global, computer->port()));
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
@@ -424,13 +440,15 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
|
||||
if (!added) {
|
||||
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
||||
for (const QHostAddress& address : addresses) {
|
||||
for (const QHostAddress& address : std::as_const(addresses)) {
|
||||
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
// Use a link-local or site-local address for the "local address"
|
||||
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
||||
addNewHost(NvAddress(address, computer->port()),
|
||||
true, computer->hostname(),
|
||||
NvAddress(v6Global, computer->port()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -551,7 +569,7 @@ void ComputerManager::handleAboutToQuit()
|
||||
|
||||
// Interrupt polling threads immediately, so they
|
||||
// avoid making additional requests while quitting
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||
entry->interrupt();
|
||||
}
|
||||
}
|
||||
@@ -703,7 +721,7 @@ void ComputerManager::stopPollingAsync()
|
||||
m_MdnsServer.reset();
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||
entry->interrupt();
|
||||
}
|
||||
}
|
||||
@@ -715,6 +733,10 @@ void ComputerManager::addNewHostManually(QString address)
|
||||
// If there wasn't a port specified, use the default
|
||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||
}
|
||||
else if (QHostAddress(address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
// The user specified an IPv6 literal without URL escaping, so use the default port
|
||||
addNewHost(NvAddress(address, DEFAULT_HTTP_PORT), false);
|
||||
}
|
||||
else {
|
||||
emit computerAddCompleted(false, false);
|
||||
}
|
||||
@@ -725,8 +747,9 @@ class PendingAddTask : public QObject, public QRunnable
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
|
||||
PendingAddTask(ComputerManager* computerManager, QString name, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Name(name),
|
||||
m_Address(address),
|
||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||
m_Mdns(mdns),
|
||||
@@ -804,7 +827,18 @@ private:
|
||||
{
|
||||
NvHTTP http(m_Address, 0, QSslCertificate());
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address.toString() << "from" << (m_Mdns ? "mDNS" : "user") << "with IPv6 address" << m_MdnsIpv6Address.toString();
|
||||
if (m_Mdns) {
|
||||
if (m_MdnsIpv6Address.isNull()) {
|
||||
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString();
|
||||
}
|
||||
else {
|
||||
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString()
|
||||
<< "and IPv6 address" << m_MdnsIpv6Address.toString();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qInfo() << "Processing new PC at" << m_Address.toString() << "from user";
|
||||
}
|
||||
|
||||
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||
QString serverInfo = fetchServerInfo(http);
|
||||
@@ -952,28 +986,24 @@ private:
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Name;
|
||||
NvAddress m_Address;
|
||||
NvAddress m_MdnsIpv6Address;
|
||||
bool m_Mdns;
|
||||
bool m_AboutToQuit;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
|
||||
void ComputerManager::addNewHost(NvAddress address, bool mdns, QString name, NvAddress mdnsIpv6Address)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for serverinfo query to complete
|
||||
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
|
||||
PendingAddTask* addTask = new PendingAddTask(this, name, address, mdnsIpv6Address, mdns);
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerManager::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
return QString::asprintf("%04u", QRandomGenerator::system()->bounded(10000));
|
||||
}
|
||||
|
||||
#include "computermanager.moc"
|
||||
|
||||
@@ -90,7 +90,6 @@ private slots:
|
||||
|
||||
void handleResolvedAddress(const QHostAddress& address)
|
||||
{
|
||||
qInfo() << "Resolved" << hostname() << "to" << address;
|
||||
m_Addresses.push_back(address);
|
||||
}
|
||||
|
||||
@@ -151,7 +150,7 @@ public:
|
||||
// interrupt() should have taken care of this
|
||||
Q_ASSERT(m_ActiveThread == nullptr);
|
||||
|
||||
for (QThread* thread : m_InactiveList) {
|
||||
for (QThread* thread : std::as_const(m_InactiveList)) {
|
||||
thread->wait();
|
||||
delete thread;
|
||||
}
|
||||
@@ -228,7 +227,7 @@ public:
|
||||
|
||||
Q_INVOKABLE void addNewHostManually(QString address);
|
||||
|
||||
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
|
||||
void addNewHost(NvAddress address, bool mdns, QString name = QString(), NvAddress mdnsIpv6Address = NvAddress());
|
||||
|
||||
QString generatePinString();
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, Q
|
||||
m_TimeoutTimer(new QTimer(this))
|
||||
{
|
||||
// If we know this computer, send a WOL packet to wake it up in case it is asleep.
|
||||
for (NvComputer * computer: m_ComputerManager->getComputers()) {
|
||||
const auto computers = m_ComputerManager->getComputers();
|
||||
for (NvComputer* computer : computers) {
|
||||
if (this->matchComputer(computer)) {
|
||||
computer->wake();
|
||||
}
|
||||
@@ -51,7 +52,8 @@ bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const NvAddress& addr : computer->uniqueAddresses()) {
|
||||
const auto uniqueAddresses = computer->uniqueAddresses();
|
||||
for (const NvAddress& addr : uniqueAddresses) {
|
||||
if (addr.address().toLower() == value || addr.toString().toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
X509* cert = X509_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
EVP_PKEY* pk = EVP_RSA_gen(2048);
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
#else
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
@@ -43,6 +47,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
|
||||
EVP_PKEY_CTX_free(ctx);
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
#endif
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||
@@ -50,28 +55,20 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
#else
|
||||
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(before);
|
||||
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(after);
|
||||
|
||||
X509_gmtime_adj(before, 0);
|
||||
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
|
||||
X509_set1_notBefore(cert, before);
|
||||
X509_set1_notAfter(cert, after);
|
||||
|
||||
ASN1_STRING_free(before);
|
||||
ASN1_STRING_free(after);
|
||||
X509_gmtime_adj(X509_getm_notBefore(cert), 0);
|
||||
X509_gmtime_adj(X509_getm_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
#endif
|
||||
|
||||
X509_set_pubkey(cert, pk);
|
||||
|
||||
X509_NAME* name = X509_get_subject_name(cert);
|
||||
X509_NAME* name = X509_NAME_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(name);
|
||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
|
||||
-1, -1, 0);
|
||||
X509_set_subject_name(cert, name);
|
||||
X509_set_issuer_name(cert, name);
|
||||
X509_NAME_free(name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha256());
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
appList[i].serialize(settings);
|
||||
appList.at(i).serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
@@ -141,7 +141,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (const QString& macOctet : macOctets) {
|
||||
for (const QString& macOctet : std::as_const(macOctets)) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
@@ -254,14 +254,16 @@ bool NvComputer::wake() const
|
||||
// case the host has timed out in ARP entries.
|
||||
QMap<QString, quint16> addressMap;
|
||||
QSet<quint16> basePortSet;
|
||||
for (const NvAddress& addr : uniqueAddresses()) {
|
||||
const auto uniqueHostAddresses = uniqueAddresses();
|
||||
for (const NvAddress& addr : uniqueHostAddresses) {
|
||||
addressMap.insert(addr.address(), addr.port());
|
||||
basePortSet.insert(addr.port());
|
||||
}
|
||||
addressMap.insert("255.255.255.255", 0);
|
||||
|
||||
// Try to broadcast on all available NICs
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
const auto allInterfaces = QNetworkInterface::allInterfaces();
|
||||
for (const QNetworkInterface& nic : allInterfaces) {
|
||||
// Ensure the interface is up and skip the loopback adapter
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||
@@ -269,7 +271,8 @@ bool NvComputer::wake() const
|
||||
}
|
||||
|
||||
QHostAddress allNodesMulticast("FF02::1");
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
const auto allInterfaceAddresses = nic.addressEntries();
|
||||
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
|
||||
// Store the scope ID for this NIC if IPv6 is enabled
|
||||
if (!addr.ip().scopeId().isEmpty()) {
|
||||
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||
@@ -375,13 +378,15 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
Q_ASSERT(!s.peerAddress().isNull());
|
||||
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
const auto allInterfaces = QNetworkInterface::allInterfaces();
|
||||
for (const QNetworkInterface& nic : allInterfaces) {
|
||||
// Ensure the interface is up
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
const auto allInterfaceAddresses = nic.addressEntries();
|
||||
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
|
||||
if (addr.ip() == s.localAddress()) {
|
||||
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
||||
|
||||
@@ -463,7 +468,7 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
}
|
||||
|
||||
// Propagate client-side attributes to the new app list
|
||||
for (const NvApp& existingApp : appList) {
|
||||
for (const NvApp& existingApp : std::as_const(appList)) {
|
||||
for (NvApp& newApp : newAppList) {
|
||||
if (existingApp.id == newApp.id) {
|
||||
newApp.hidden = existingApp.hidden;
|
||||
|
||||
+19
-19
@@ -18,7 +18,8 @@
|
||||
#define RESUME_TIMEOUT_MS 30000
|
||||
#define QUIT_TIMEOUT_MS 30000
|
||||
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) :
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam) :
|
||||
m_Nam(nam ? nam : new QNetworkAccessManager(this)),
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
@@ -29,13 +30,11 @@ NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert
|
||||
|
||||
// Never use a proxy server
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
m_Nam.setProxy(noProxy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
m_Nam->setProxy(noProxy);
|
||||
}
|
||||
|
||||
NvHTTP::NvHTTP(NvComputer* computer) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
|
||||
NvHTTP::NvHTTP(NvComputer* computer, QNetworkAccessManager* nam) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert, nam)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -304,8 +303,7 @@ NvHTTP::getAppList()
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
Q_ASSERT(false);
|
||||
return QVector<NvApp>();
|
||||
throw std::runtime_error("Invalid applist XML");
|
||||
}
|
||||
apps.append(NvApp());
|
||||
}
|
||||
@@ -391,7 +389,7 @@ NvHTTP::getXmlStringFromHex(QString xml,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return QByteArray::fromHex(str.toLatin1());
|
||||
return QByteArray::fromHex(str.toUtf8());
|
||||
}
|
||||
|
||||
QString
|
||||
@@ -493,15 +491,15 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
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
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
|
||||
// Use fine-grained idle timeouts to avoid calling QNetworkAccessManager::clearAccessCache(),
|
||||
// which tears down the NAM's global thread each time. We must not keep persistent connections
|
||||
// or GFE will puke.
|
||||
request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, 0);
|
||||
#endif
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
auto sslErrorsConnection = connect(m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
QNetworkReply* reply = m_Nam->get(request);
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
@@ -524,9 +522,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
// We must clear out cached authentication and connections or
|
||||
// GFE will puke next time
|
||||
m_Nam.clearAccessCache();
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
||||
// If we couldn't use fine-grained connection idle timeouts, kill them all now
|
||||
m_Nam->clearAccessCache();
|
||||
#endif
|
||||
disconnect(sslErrorsConnection);
|
||||
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
|
||||
+13
-13
@@ -33,19 +33,19 @@ class GfeHttpResponseException : public std::exception
|
||||
public:
|
||||
GfeHttpResponseException(int statusCode, QString message) :
|
||||
m_StatusCode(statusCode),
|
||||
m_StatusMessage(message)
|
||||
m_StatusMessage(message.toUtf8())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
const char* what() const throw()
|
||||
{
|
||||
return m_StatusMessage.toLatin1();
|
||||
return m_StatusMessage.constData();
|
||||
}
|
||||
|
||||
const char* getStatusMessage() const
|
||||
{
|
||||
return m_StatusMessage.toLatin1();
|
||||
return m_StatusMessage.constData();
|
||||
}
|
||||
|
||||
int getStatusCode() const
|
||||
@@ -55,12 +55,12 @@ public:
|
||||
|
||||
QString toQString() const
|
||||
{
|
||||
return m_StatusMessage + " (Error " + QString::number(m_StatusCode) + ")";
|
||||
return QString::fromUtf8(m_StatusMessage) + " (Error " + QString::number(m_StatusCode) + ")";
|
||||
}
|
||||
|
||||
private:
|
||||
int m_StatusCode;
|
||||
QString m_StatusMessage;
|
||||
QByteArray m_StatusMessage;
|
||||
};
|
||||
|
||||
class QtNetworkReplyException : public std::exception
|
||||
@@ -68,19 +68,19 @@ class QtNetworkReplyException : public std::exception
|
||||
public:
|
||||
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
|
||||
m_Error(error),
|
||||
m_ErrorText(errorText)
|
||||
m_ErrorText(errorText.toUtf8())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
const char* what() const throw()
|
||||
{
|
||||
return m_ErrorText.toLatin1();
|
||||
return m_ErrorText.constData();
|
||||
}
|
||||
|
||||
const char* getErrorText() const
|
||||
{
|
||||
return m_ErrorText.toLatin1();
|
||||
return m_ErrorText.constData();
|
||||
}
|
||||
|
||||
QNetworkReply::NetworkError getError() const
|
||||
@@ -90,12 +90,12 @@ public:
|
||||
|
||||
QString toQString() const
|
||||
{
|
||||
return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
|
||||
return QString::fromUtf8(m_ErrorText) + " (Error " + QString::number(m_Error) + ")";
|
||||
}
|
||||
|
||||
private:
|
||||
QNetworkReply::NetworkError m_Error;
|
||||
QString m_ErrorText;
|
||||
QByteArray m_ErrorText;
|
||||
};
|
||||
|
||||
class NvHTTP : public QObject
|
||||
@@ -109,9 +109,9 @@ public:
|
||||
NVLL_VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert);
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam = nullptr);
|
||||
|
||||
explicit NvHTTP(NvComputer* computer);
|
||||
explicit NvHTTP(NvComputer* computer, QNetworkAccessManager* nam = nullptr);
|
||||
|
||||
static
|
||||
int
|
||||
@@ -196,6 +196,6 @@ private:
|
||||
NvLogLevel logLevel);
|
||||
|
||||
NvAddress m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@@ -101,6 +101,29 @@ NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromCert(X509* cert)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#endif
|
||||
|
||||
return QByteArray(
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
reinterpret_cast<const char*>(ASN1_STRING_data(asnSignature)),
|
||||
#else
|
||||
reinterpret_cast<const char*>(ASN1_STRING_get0_data(asnSignature)),
|
||||
#endif
|
||||
ASN1_STRING_length(asnSignature)
|
||||
);
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||
{
|
||||
@@ -114,18 +137,7 @@ NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#endif
|
||||
|
||||
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
|
||||
QByteArray signature = getSignatureFromCert(cert);
|
||||
X509_free(cert);
|
||||
|
||||
return signature;
|
||||
@@ -184,7 +196,7 @@ NvPairingManager::signMessage(const QByteArray& message)
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
return QByteArray().append(salt).append(pin.toUtf8());
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
@@ -267,18 +279,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||
#endif
|
||||
|
||||
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
challengeResponse.append(getSignatureFromCert(m_Cert));
|
||||
challengeResponse.append(clientSecretData);
|
||||
|
||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||
|
||||
@@ -37,6 +37,9 @@ private:
|
||||
QByteArray
|
||||
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromCert(X509* cert);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(const QByteArray& certificate);
|
||||
|
||||
|
||||
@@ -12,6 +12,38 @@
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
class SystemPropertyQueryThread : public QThread
|
||||
{
|
||||
public:
|
||||
SystemPropertyQueryThread(SystemProperties* properties)
|
||||
: QThread(properties), m_Properties(properties)
|
||||
{
|
||||
setObjectName("System Properties Async Query Thread");
|
||||
}
|
||||
|
||||
private:
|
||||
void run() override
|
||||
{
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
bool supportsHdr;
|
||||
QSize maximumResolution;
|
||||
|
||||
Session::getDecoderInfo(m_Properties->testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
|
||||
|
||||
// Propagate the decoder properties to the SystemProperties singleton and emit any change signals on the main thread
|
||||
QMetaObject::invokeMethod(m_Properties, "updateDecoderProperties",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(bool, hasHardwareAcceleration),
|
||||
Q_ARG(bool, rendererAlwaysFullScreen),
|
||||
Q_ARG(QSize, maximumResolution),
|
||||
Q_ARG(bool, supportsHdr));
|
||||
}
|
||||
|
||||
private:
|
||||
SystemProperties* m_Properties;
|
||||
};
|
||||
|
||||
SystemProperties::SystemProperties()
|
||||
{
|
||||
versionString = QString(VERSION_STR);
|
||||
@@ -25,9 +57,8 @@ SystemProperties::SystemProperties()
|
||||
{
|
||||
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)) {
|
||||
// Use IsWow64Process2() because it doesn't lie on ARM64
|
||||
if (IsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
|
||||
switch (machineArch) {
|
||||
case IMAGE_FILE_MACHINE_I386:
|
||||
nativeArch = "i386";
|
||||
@@ -66,68 +97,86 @@ SystemProperties::SystemProperties()
|
||||
hasDiscordIntegration = false;
|
||||
#endif
|
||||
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
// These will be queried asynchronously to avoid blocking the UI
|
||||
hasHardwareAcceleration = true;
|
||||
rendererAlwaysFullScreen = false;
|
||||
supportsHdr = true;
|
||||
maximumResolution = QSize(0, 0);
|
||||
}
|
||||
|
||||
// Populate data that requires talking to SDL. We do it all in one shot
|
||||
// and cache the results to speed up future queries on this data.
|
||||
querySdlVideoInfo();
|
||||
SystemProperties::~SystemProperties()
|
||||
{
|
||||
waitForAsyncLoad();
|
||||
}
|
||||
|
||||
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
|
||||
void SystemProperties::updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr)
|
||||
{
|
||||
SDL_assert(testWindow);
|
||||
|
||||
if (hasHardwareAcceleration != this->hasHardwareAcceleration) {
|
||||
this->hasHardwareAcceleration = hasHardwareAcceleration;
|
||||
emit hasHardwareAccelerationChanged();
|
||||
}
|
||||
|
||||
if (rendererAlwaysFullScreen != this->rendererAlwaysFullScreen) {
|
||||
this->rendererAlwaysFullScreen = rendererAlwaysFullScreen;
|
||||
emit rendererAlwaysFullScreenChanged();
|
||||
}
|
||||
|
||||
if (maximumResolution != this->maximumResolution) {
|
||||
this->maximumResolution = maximumResolution;
|
||||
emit maximumResolutionChanged();
|
||||
}
|
||||
|
||||
if (supportsHdr != this->supportsHdr) {
|
||||
this->supportsHdr = supportsHdr;
|
||||
emit supportsHdrChanged();
|
||||
}
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
testWindow = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
|
||||
return monitorSafeAreaResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
int SystemProperties::getRefreshRate(int displayIndex)
|
||||
{
|
||||
// Returns 0 if out of bounds
|
||||
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||
return monitorRefreshRates.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
void SystemProperties::startAsyncLoad()
|
||||
{
|
||||
public:
|
||||
QuerySdlVideoThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->querySdlVideoInfoInternal();
|
||||
if (systemPropertyQueryThread) {
|
||||
// Already started/completed
|
||||
return;
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
QuerySdlVideoThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
// This isn't actually asynchronous (due to the need to synchronize with
|
||||
// SdlGamepadKeyNavigation), but we don't query it in the constructor
|
||||
// because it's expensive.
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
if (!unmappedGamepads.isEmpty()) {
|
||||
emit unmappedGamepadsChanged();
|
||||
}
|
||||
else {
|
||||
querySdlVideoInfoInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::querySdlVideoInfoInternal()
|
||||
{
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
// We initialize the video subsystem and test window on the main thread
|
||||
// because some platforms (macOS) do not support window creation on
|
||||
// non-main threads.
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
@@ -135,64 +184,34 @@ void SystemProperties::querySdlVideoInfoInternal()
|
||||
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());
|
||||
testWindow = StreamUtils::createTestWindow();
|
||||
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;
|
||||
}
|
||||
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, supportsHdr, maximumResolution);
|
||||
// Update display related attributes (max FPS, native resolution, etc).
|
||||
//
|
||||
// NB: SDL3 will forcefully refresh displays when a window is created,
|
||||
// so we place this after the window creation to ensure we don't pay
|
||||
// the penalty for mode enumeration twice.
|
||||
refreshDisplays();
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
systemPropertyQueryThread = new SystemPropertyQueryThread(this);
|
||||
systemPropertyQueryThread->start();
|
||||
}
|
||||
|
||||
class RefreshDisplaysThread : public QThread
|
||||
void SystemProperties::waitForAsyncLoad()
|
||||
{
|
||||
public:
|
||||
RefreshDisplaysThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->refreshDisplaysInternal();
|
||||
if (systemPropertyQueryThread) {
|
||||
systemPropertyQueryThread->wait();
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -221,7 +240,8 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
|
||||
// Start at desktop mode and work our way up
|
||||
bestMode = desktopMode;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
int numDisplayModes = SDL_GetNumDisplayModes(displayIndex);
|
||||
for (int i = 0; i < numDisplayModes; i++) {
|
||||
SDL_DisplayMode mode;
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
|
||||
#include "SDL_compat.h"
|
||||
|
||||
class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class QuerySdlVideoThread;
|
||||
friend class RefreshDisplaysThread;
|
||||
friend class SystemPropertyQueryThread;
|
||||
|
||||
public:
|
||||
SystemProperties();
|
||||
~SystemProperties();
|
||||
|
||||
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
|
||||
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
|
||||
// Static properties queried synchronously during the constructor
|
||||
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||
@@ -22,27 +23,40 @@ public:
|
||||
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)
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
// Properties queried asynchronously (startAsyncLoad() must be called!)
|
||||
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration NOTIFY hasHardwareAccelerationChanged)
|
||||
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen NOTIFY rendererAlwaysFullScreenChanged)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution NOTIFY maximumResolutionChanged)
|
||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr NOTIFY supportsHdrChanged)
|
||||
|
||||
// Either startAsyncLoad()+waitForAsyncLoad() or refreshDisplays() must be invoked first
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getSafeAreaResolution(int displayIndex);
|
||||
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||
|
||||
Q_INVOKABLE void startAsyncLoad();
|
||||
Q_INVOKABLE void waitForAsyncLoad();
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
|
||||
signals:
|
||||
void unmappedGamepadsChanged();
|
||||
void hasHardwareAccelerationChanged();
|
||||
void rendererAlwaysFullScreenChanged();
|
||||
void maximumResolutionChanged();
|
||||
void supportsHdrChanged();
|
||||
|
||||
private slots:
|
||||
void updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr);
|
||||
|
||||
private:
|
||||
void querySdlVideoInfo();
|
||||
void querySdlVideoInfoInternal();
|
||||
void refreshDisplaysInternal();
|
||||
QThread* systemPropertyQueryThread = nullptr;
|
||||
SDL_Window* testWindow = nullptr;
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
// Properties set by the constructor
|
||||
bool isRunningWayland;
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
@@ -50,13 +64,19 @@ private:
|
||||
bool hasDesktopEnvironment;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
QString versionString;
|
||||
bool usesMaterial3Theme;
|
||||
|
||||
// Properties only set if startAsyncLoad() is called
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
QSize maximumResolution;
|
||||
bool supportsHdr;
|
||||
QString unmappedGamepads;
|
||||
|
||||
// Properties set by refreshDisplays()
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QList<QRect> monitorSafeAreaResolutions;
|
||||
QList<int> monitorRefreshRates;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
bool usesMaterial3Theme;
|
||||
};
|
||||
|
||||
|
||||
@@ -59,13 +59,6 @@ public:
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN32)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||
#endif
|
||||
message = message.endsWith('\n') ? message : message + '\n';
|
||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||
}
|
||||
@@ -113,7 +106,7 @@ public:
|
||||
|
||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||
{
|
||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
static QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
auto match = re.match(value(name));
|
||||
if (!match.hasMatch()) {
|
||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||
@@ -386,7 +379,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
static QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
|
||||
+19
-31
@@ -5,10 +5,8 @@
|
||||
#include "backend/computerseeker.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 30000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
@@ -16,8 +14,7 @@ namespace CliListApps
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateListApp,
|
||||
StateSeekApp,
|
||||
StateListApps,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
@@ -26,7 +23,6 @@ class Event
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
@@ -66,9 +62,6 @@ public:
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
|
||||
m_BoxArtManager = new BoxArtManager(q);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
@@ -88,12 +81,28 @@ public:
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_State = StateListApps;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Loading app list...\n");
|
||||
}
|
||||
|
||||
// To avoid race conditions where ComputerSeeker stops async polling, but
|
||||
// ComputerManager is yet to update the app list, we will explicitly fetch the latest app list.
|
||||
// Otherwise, it becomes complicated as we would have to guess whether ComputerManager
|
||||
// would emit 1 signal (the list did not change) or 2 signals (indicating that the list has changed)
|
||||
try {
|
||||
NvHTTP http{m_Computer};
|
||||
|
||||
const auto appList = http.getAppList();
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(appList) : printApps(appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
} catch (std::exception& exception) {
|
||||
fprintf(stderr, "%s\n", exception.what());
|
||||
QCoreApplication::exit(1);
|
||||
}
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
|
||||
@@ -104,14 +113,6 @@ public:
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +147,6 @@ public:
|
||||
BoxArtManager *m_BoxArtManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
@@ -157,11 +157,7 @@ Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *p
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
d->m_Arguments = arguments;
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
@@ -197,12 +193,4 @@ void Launcher::onComputerSeekTimeout()
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ public:
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
|
||||
private:
|
||||
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (const NvApp& app : m_Computer->appList) {
|
||||
for (const NvApp& app : std::as_const(m_Computer->appList)) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
From 78261eb6e2fe728fa5d3843b5b8962b1ca559b7e Mon Sep 17 00:00:00 2001
|
||||
From: David Rosca <nowrep@gmail.com>
|
||||
Date: Sat, 16 Dec 2023 14:52:01 +0100
|
||||
Subject: [PATCH] Vulkan: Don't try to reuse old swapchain
|
||||
|
||||
---
|
||||
src/vulkan/swapchain.c | 12 ++++++++++--
|
||||
1 file changed, 10 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/vulkan/swapchain.c b/src/vulkan/swapchain.c
|
||||
index 0646e57c..c0b07ce4 100644
|
||||
--- a/src/vulkan/swapchain.c
|
||||
+++ b/src/vulkan/swapchain.c
|
||||
@@ -612,13 +612,21 @@ static bool vk_sw_recreate(pl_swapchain sw, int w, int h)
|
||||
}
|
||||
#endif
|
||||
|
||||
+ {
|
||||
+ pl_gpu_flush(gpu);
|
||||
+ vk_wait_idle(vk);
|
||||
+ for (int i = 0; i < vk->pool_graphics->num_queues; i++)
|
||||
+ vk->QueueWaitIdle(vk->pool_graphics->queues[i]);
|
||||
+ vk->DestroySwapchainKHR(vk->dev, p->swapchain, PL_VK_ALLOC);
|
||||
+ }
|
||||
+
|
||||
// Calling `vkCreateSwapchainKHR` puts sinfo.oldSwapchain into a retired
|
||||
// state whether the call succeeds or not, so we always need to garbage
|
||||
// collect it afterwards - asynchronously as it may still be in use
|
||||
- sinfo.oldSwapchain = p->swapchain;
|
||||
+ /* sinfo.oldSwapchain = p->swapchain; */
|
||||
p->swapchain = VK_NULL_HANDLE;
|
||||
VkResult res = vk->CreateSwapchainKHR(vk->dev, &sinfo, PL_VK_ALLOC, &p->swapchain);
|
||||
- vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain));
|
||||
+ /* vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain)); */
|
||||
PL_VK_ASSERT(res, "vk->CreateSwapchainKHR(...)");
|
||||
|
||||
// Get the new swapchain images
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<component type="desktop-application">
|
||||
<id>com.moonlight_stream.Moonlight</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
@@ -10,7 +10,7 @@
|
||||
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||
<p>Features include:</p>
|
||||
<ul>
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and Vulkan Video support</li>
|
||||
<li>H.264, HEVC, and AV1 codec support</li>
|
||||
<li>7.1 surround sound audio support</li>
|
||||
<li>10-point multitouch support</li>
|
||||
@@ -23,13 +23,30 @@
|
||||
<url type="homepage">https://moonlight-stream.org</url>
|
||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||
<url type="vcs-browser">https://github.com/moonlight-stream/moonlight-qt</url>
|
||||
<developer id="org.moonlight-stream">
|
||||
<name>Moonlight Game Streaming Team</name>
|
||||
</developer>
|
||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
<provides>
|
||||
<binary>moonlight</binary>
|
||||
</provides>
|
||||
<content_rating type="oars-1.1" />
|
||||
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#710085</color>
|
||||
<color type="primary" scheme_preference="dark">#55285d</color>
|
||||
</branding>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
<caption>Host selection page showing local PCs</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
<caption>Settings page displaying streaming options</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
@@ -816,19 +833,4 @@
|
||||
</release>
|
||||
</releases>
|
||||
|
||||
<categories>
|
||||
<category>Game</category>
|
||||
<category>Qt</category>
|
||||
</categories>
|
||||
|
||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
|
||||
<provides>
|
||||
<binary>moonlight</binary>
|
||||
</provides>
|
||||
|
||||
<content_rating type="oars-1.0" />
|
||||
</component>
|
||||
|
||||
@@ -5,5 +5,5 @@ Exec=moonlight
|
||||
Icon=moonlight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Qt;Game;
|
||||
Categories=Qt;Game;Network;RemoteAccess;
|
||||
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||
|
||||
+30
-24
@@ -38,7 +38,7 @@ CenteredGridView {
|
||||
activated = true
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ CenteredGridView {
|
||||
// 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
|
||||
((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
|
||||
}
|
||||
@@ -123,20 +123,18 @@ CenteredGridView {
|
||||
|
||||
sourceComponent: Item {
|
||||
RoundButton {
|
||||
// Don't steal focus from the toolbar buttons
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
icon.source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
icon.width: 75
|
||||
icon.height: 75
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
@@ -151,20 +149,18 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
RoundButton {
|
||||
// Don't steal focus from the toolbar buttons
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
icon.source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
icon.width: 75
|
||||
icon.height: 75
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
@@ -298,19 +294,17 @@ CenteredGridView {
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
initiator: appContextMenuLoader.parent
|
||||
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")
|
||||
@@ -323,7 +317,6 @@ CenteredGridView {
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
@@ -339,6 +332,19 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
visible: appGrid.count === 0
|
||||
|
||||
Label {
|
||||
text: qsTr("This computer doesn't seem to have any applications or some applications are hidden")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
property string appName : ""
|
||||
|
||||
@@ -2,10 +2,6 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
GridView {
|
||||
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
|
||||
// Prior to 5.12, the leftMargin and rightMargin values did not work.
|
||||
property bool hasBrokenMargins: this.synchronousDrag === undefined
|
||||
|
||||
property int minMargin: 10
|
||||
property real availableWidth: (parent.width - 2 * minMargin)
|
||||
property int itemsPerRow: availableWidth / cellWidth
|
||||
@@ -15,11 +11,6 @@ GridView {
|
||||
function updateMargins() {
|
||||
leftMargin = horizontalMargin
|
||||
rightMargin = horizontalMargin
|
||||
|
||||
if (hasBrokenMargins) {
|
||||
anchors.leftMargin = leftMargin
|
||||
anchors.rightMargin = rightMargin
|
||||
}
|
||||
}
|
||||
|
||||
onHorizontalMarginChanged: {
|
||||
@@ -27,15 +18,6 @@ GridView {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (hasBrokenMargins) {
|
||||
// This will cause an anchor conflict with the parent StackView
|
||||
// which breaks animation, but otherwise the grid will not be
|
||||
// centered in the window.
|
||||
anchors.fill = parent
|
||||
anchors.topMargin = topMargin
|
||||
anchors.bottomMargin = bottomMargin
|
||||
}
|
||||
|
||||
updateMargins()
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@@ -34,6 +34,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@@ -52,6 +52,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls 2.5
|
||||
|
||||
Dialog {
|
||||
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
|
||||
parent: ApplicationWindow.contentItem
|
||||
modal: true
|
||||
anchors.centerIn: Overlay.overlay
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
onAboutToHide: {
|
||||
// We must force focus back to the last item for platforms without
|
||||
// support for more than one active window like Steam Link. If
|
||||
// we don't, gamepad and keyboard navigation will break after a
|
||||
onClosed: {
|
||||
// We must force focus back to the last item. If we don't,
|
||||
// gamepad and keyboard navigation will break after a
|
||||
// dialog appears.
|
||||
stackView.forceActiveFocus()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ ItemDelegate {
|
||||
}
|
||||
Keys.onUpPressed: {
|
||||
grid.moveCurrentIndexUp()
|
||||
|
||||
// If we've reached the top of the grid, move focus to the toolbar
|
||||
if (grid.currentItem === this) {
|
||||
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
|
||||
@@ -2,13 +2,18 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
property var initiator
|
||||
|
||||
onOpened: {
|
||||
// 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
|
||||
// If the initiating object currently has keyboard focus,
|
||||
// give focus to the first visible and enabled menu item
|
||||
if (initiator.focus) {
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = itemAt(i)
|
||||
if (item.visible && item.enabled) {
|
||||
item.forceActiveFocus(Qt.TabFocusReason)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||
// so we must make our own.
|
||||
property Menu parentMenu
|
||||
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
@@ -15,7 +11,7 @@ MenuItem {
|
||||
// We must close the context menu first or
|
||||
// it can steal focus from any dialogs that
|
||||
// onTriggered may spawn.
|
||||
parentMenu.close()
|
||||
menu.close()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
@@ -27,6 +23,6 @@ MenuItem {
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
menu.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ NavigableDialog {
|
||||
|
||||
onOpened: {
|
||||
// Force keyboard focus on the label so keyboard navigation works
|
||||
dialogLabel.forceActiveFocus()
|
||||
if (dialogButtonBox.count > 0) {
|
||||
dialogButtonBox.itemAt(dialogButtonBox.count - 1).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
@@ -24,6 +26,7 @@ NavigableDialog {
|
||||
BusyIndicator {
|
||||
id: dialogSpinner
|
||||
visible: false
|
||||
running: visible
|
||||
}
|
||||
|
||||
Image {
|
||||
@@ -51,18 +54,6 @@ NavigableDialog {
|
||||
// will cause word wrap to kick in.
|
||||
Layout.maximumWidth: 400
|
||||
Layout.maximumHeight: 400
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +61,15 @@ NavigableDialog {
|
||||
id: dialogButtonBox
|
||||
standardButtons: dialog.standardButtons
|
||||
|
||||
delegate: Button {
|
||||
flat: true
|
||||
|
||||
Keys.onReturnPressed: clicked()
|
||||
Keys.onEnterPressed: clicked()
|
||||
Keys.onRightPressed: nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
Keys.onLeftPressed: nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
onHelpRequested: {
|
||||
Qt.openUrlExternally(helpUrl)
|
||||
close()
|
||||
|
||||
@@ -7,17 +7,9 @@ ToolButton {
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
// FIXME: We're using an Image here rather than icon.source because
|
||||
// icons don't work on Qt 5.9 LTS.
|
||||
Image {
|
||||
id: image
|
||||
source: iconSource
|
||||
anchors.centerIn: parent.background
|
||||
sourceSize {
|
||||
width: parent.background.width * 1.10
|
||||
height: parent.background.height * 1.10
|
||||
}
|
||||
}
|
||||
icon.source: iconSource
|
||||
icon.width: background.width
|
||||
icon.height: background.height
|
||||
|
||||
// This determines the size of the Material highlight. We increase it
|
||||
// from the default because we use larger than normal icons for TV readability.
|
||||
|
||||
+4
-11
@@ -35,7 +35,7 @@ CenteredGridView {
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ CenteredGridView {
|
||||
BusyIndicator {
|
||||
id: searchSpinner
|
||||
visible: StreamingPreferences.enableMdns
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -143,6 +144,7 @@ CenteredGridView {
|
||||
width: 75
|
||||
height: 75
|
||||
visible: model.statusUnknown
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -163,13 +165,13 @@ CenteredGridView {
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: pcContextMenu
|
||||
initiator: pcContextMenuLoader.parent
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
@@ -179,13 +181,11 @@ CenteredGridView {
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
@@ -194,7 +194,6 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
@@ -203,7 +202,6 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
@@ -212,7 +210,6 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View Details")
|
||||
onTriggered: {
|
||||
showPcDetailsDialog.pcDetails = model.details
|
||||
@@ -293,10 +290,6 @@ CenteredGridView {
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: pairDialog
|
||||
|
||||
// Pairing dialog must be modal to prevent double-clicks from triggering
|
||||
// pairing twice
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
|
||||
@@ -60,6 +60,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
+81
-31
@@ -281,11 +281,13 @@ Flickable {
|
||||
StreamingPreferences.width = selectedWidth
|
||||
StreamingPreferences.height = selectedHeight
|
||||
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
@@ -447,11 +449,13 @@ Flickable {
|
||||
if (StreamingPreferences.fps !== selectedFps) {
|
||||
StreamingPreferences.fps = selectedFps
|
||||
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
@@ -678,26 +682,47 @@ Flickable {
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
value: StreamingPreferences.bitrateKbps
|
||||
Slider {
|
||||
id: slider
|
||||
|
||||
stepSize: 500
|
||||
from : 500
|
||||
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
|
||||
value: StreamingPreferences.bitrateKbps
|
||||
|
||||
snapMode: "SnapOnRelease"
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
stepSize: 500
|
||||
from : 500
|
||||
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
|
||||
|
||||
onValueChanged: {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
snapMode: "SnapOnRelease"
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width - (resetBitrateButton.visible ? resetBitrateButton.width + parent.spacing : 0))
|
||||
|
||||
onValueChanged: {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
}
|
||||
|
||||
onMoved: {
|
||||
StreamingPreferences.autoAdjustBitrate = false
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(valueChanged)
|
||||
Button {
|
||||
id: resetBitrateButton
|
||||
text: qsTr("Use Default (%1 Mbps)").arg(StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444) / 1000.0)
|
||||
visible: StreamingPreferences.bitrateKbps !== StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
|
||||
onClicked: {
|
||||
var defaultBitrate = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
|
||||
StreamingPreferences.bitrateKbps = defaultBitrate
|
||||
StreamingPreferences.autoAdjustBitrate = true
|
||||
slider.value = defaultBitrate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1072,10 +1097,10 @@ Flickable {
|
||||
text: "Português" // Portuguese
|
||||
val: StreamingPreferences.LANG_PT
|
||||
}
|
||||
/* ListElement {
|
||||
ListElement {
|
||||
text: "Português do Brasil" // Brazilian Portuguese
|
||||
val: StreamingPreferences.LANG_PT_BR
|
||||
} */
|
||||
}
|
||||
ListElement {
|
||||
text: "Ελληνικά" // Greek
|
||||
val: StreamingPreferences.LANG_EL
|
||||
@@ -1112,6 +1137,18 @@ Flickable {
|
||||
text: "Eesti" // Estonian
|
||||
val: StreamingPreferences.LANG_ET
|
||||
} */
|
||||
ListElement {
|
||||
text: "Български" // Bulgarian
|
||||
val: StreamingPreferences.LANG_BG
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Esperanto"
|
||||
val: StreamingPreferences.LANG_EO
|
||||
} */
|
||||
ListElement {
|
||||
text: "தமிழ்" // Tamil
|
||||
val: StreamingPreferences.LANG_TA
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
@@ -1199,6 +1236,17 @@ Flickable {
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: configurationWarningsCheck
|
||||
width: parent.width
|
||||
text: qsTr("Show configuration warnings")
|
||||
font.pointSize: 12
|
||||
checked: StreamingPreferences.configurationWarnings
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.configurationWarnings = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
visible: SystemProperties.hasDiscordIntegration
|
||||
id: discordPresenceCheck
|
||||
@@ -1616,11 +1664,13 @@ Flickable {
|
||||
// This is called on init, so only reset to default bitrate when checked state changes.
|
||||
if (StreamingPreferences.enableYUV444 != checked) {
|
||||
StreamingPreferences.enableYUV444 = checked
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+60
-33
@@ -4,6 +4,7 @@ import QtQuick.Window 2.2
|
||||
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import Session 1.0
|
||||
import SystemProperties 1.0
|
||||
|
||||
Item {
|
||||
property Session session
|
||||
@@ -48,18 +49,6 @@ Item {
|
||||
console.error(text)
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
{
|
||||
// This toast appears for 3 seconds, just shorter than how long
|
||||
// Session will wait for it to be displayed. This gives it time
|
||||
// to transition to invisible before continuing.
|
||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||
toast.text = text
|
||||
toast.timeout = 3000
|
||||
toast.visible = true
|
||||
console.warn(text)
|
||||
}
|
||||
|
||||
function quitStarting()
|
||||
{
|
||||
// Avoid the push transition animation
|
||||
@@ -79,20 +68,16 @@ Item {
|
||||
// Re-enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
if (quitAfter) {
|
||||
if (streamSegueErrorDialog.text) {
|
||||
// Quit when the error dialog is acknowledged
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
else {
|
||||
// Quit immediately
|
||||
Qt.quit()
|
||||
}
|
||||
} else {
|
||||
// Exit this view
|
||||
// Pop the StreamSegue off the stack if this is a GUI-based app launch
|
||||
if (!quitAfter) {
|
||||
stackView.pop()
|
||||
}
|
||||
|
||||
if (quitAfter && !streamSegueErrorDialog.text) {
|
||||
// If this was a CLI launch without errors, exit now
|
||||
Qt.quit()
|
||||
}
|
||||
else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
@@ -132,11 +117,14 @@ Item {
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||
|
||||
// Ensure the SystemProperties async thread is finished,
|
||||
// since it may currently be using the SDL video subsystem
|
||||
SystemProperties.waitForAsyncLoad()
|
||||
|
||||
// Kick off the stream
|
||||
spinnerTimer.start()
|
||||
streamLoader.active = true
|
||||
@@ -151,7 +139,20 @@ Item {
|
||||
// middle of the animation on Windows, which looks very
|
||||
// obviously broken.
|
||||
interval: 100
|
||||
onTriggered: stageSpinner.running = true
|
||||
onTriggered: stageSpinner.visible = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: startSessionTimer
|
||||
onTriggered: {
|
||||
// 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.start()
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
@@ -170,13 +171,38 @@ Item {
|
||||
// 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()
|
||||
// Initialize the session and probe for host/client capabilities
|
||||
if (!session.initialize(window)) {
|
||||
sessionFinished(0);
|
||||
sessionReadyForDeletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Window.window)
|
||||
// Don't wait unless we have toasts to display
|
||||
startSessionTimer.interval = 0
|
||||
|
||||
// Display the toasts together in a vertical centered arrangement
|
||||
var yOffset = 0
|
||||
for (var i = 0; i < session.launchWarnings.length; i++) {
|
||||
var text = session.launchWarnings[i]
|
||||
console.warn(text)
|
||||
|
||||
// Show the tooltip for 3 seconds
|
||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||
toast.timeout = 3000
|
||||
toast.text = text
|
||||
toast.y += yOffset
|
||||
toast.visible = true
|
||||
|
||||
// Offset the next toast below the previous one
|
||||
yOffset = toast.y + toast.padding + toast.height
|
||||
|
||||
// Allow an extra 500 ms for the tooltip's fade-out animation to finish
|
||||
startSessionTimer.interval = toast.timeout + 500;
|
||||
}
|
||||
|
||||
// Start the timer to wait for toasts (or start the session immediately)
|
||||
startSessionTimer.start()
|
||||
}
|
||||
|
||||
sourceComponent: Item {}
|
||||
@@ -188,7 +214,8 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
running: visible
|
||||
visible: false
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@@ -120,7 +120,7 @@ void AppModel::quitRunningApp()
|
||||
|
||||
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
|
||||
{
|
||||
for (const NvApp& visibleApp : m_VisibleApps) {
|
||||
for (const NvApp& visibleApp : std::as_const(m_VisibleApps)) {
|
||||
if (app.id == visibleApp.id) {
|
||||
return true;
|
||||
}
|
||||
@@ -156,7 +156,7 @@ void AppModel::updateAppList(QVector<NvApp> newList)
|
||||
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||
|
||||
bool found = false;
|
||||
for (const NvApp& newApp : newVisibleList) {
|
||||
for (const NvApp& newApp : std::as_const(newVisibleList)) {
|
||||
if (existingApp.id == newApp.id) {
|
||||
// If the data changed, update it in our list
|
||||
if (existingApp != newApp) {
|
||||
@@ -178,7 +178,7 @@ void AppModel::updateAppList(QVector<NvApp> newList)
|
||||
}
|
||||
|
||||
// Process additions now
|
||||
for (const NvApp& newApp : newVisibleList) {
|
||||
for (const NvApp& newApp : std::as_const(newVisibleList)) {
|
||||
int insertionIndex = m_VisibleApps.size();
|
||||
bool found = false;
|
||||
|
||||
|
||||
+33
-21
@@ -51,10 +51,20 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
// Display any modal dialogs for configuration warnings
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
if (runConfigChecks) {
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
|
||||
// Hardware acceleration and unmapped gamepads are checked asynchronously
|
||||
SystemProperties.hasHardwareAccelerationChanged.connect(hasHardwareAccelerationChanged)
|
||||
SystemProperties.unmappedGamepadsChanged.connect(hasUnmappedGamepadsChanged)
|
||||
SystemProperties.startAsyncLoad()
|
||||
}
|
||||
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||
}
|
||||
|
||||
function hasHardwareAccelerationChanged() {
|
||||
if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
|
||||
if (SystemProperties.isRunningXWayland) {
|
||||
xWaylandDialog.open()
|
||||
}
|
||||
@@ -62,16 +72,28 @@ ApplicationWindow {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasUnmappedGamepadsChanged() {
|
||||
if (SystemProperties.unmappedGamepads) {
|
||||
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||
unmappedGamepadDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// It would be better to use TextMetrics here, but it always lays out
|
||||
// the text slightly more compactly than real Text does in ToolTip,
|
||||
// causing unexpected line breaks to be inserted
|
||||
Text {
|
||||
id: tooltipTextLayoutHelper
|
||||
visible: false
|
||||
font: ToolTip.toolTip.font
|
||||
text: ToolTip.toolTip.text
|
||||
}
|
||||
|
||||
// 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
|
||||
ToolTip.toolTip.contentWidth: Math.min(tooltipTextLayoutHelper.width, 400)
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
@@ -195,20 +217,10 @@ ApplicationWindow {
|
||||
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
|
||||
}
|
||||
|
||||
// Workaround for lack of instanceof in Qt 5.9.
|
||||
//
|
||||
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
|
||||
function qmltypeof(obj, className) { // QtObject, string -> bool
|
||||
// className plus "(" is the class instance without modification
|
||||
// className plus "_QML" is the class instance with user-defined properties
|
||||
var str = obj.toString();
|
||||
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
|
||||
}
|
||||
|
||||
function navigateTo(url, objectType)
|
||||
{
|
||||
var existingItem = stackView.find(function(item, index) {
|
||||
return qmltypeof(item, objectType)
|
||||
return item instanceof objectType
|
||||
})
|
||||
|
||||
if (existingItem !== null) {
|
||||
@@ -275,7 +287,7 @@ ApplicationWindow {
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: qmltypeof(stackView.currentItem, "SettingsView")
|
||||
visible: stackView.currentItem instanceof SettingsView
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
@@ -285,7 +297,7 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
qmltypeof(stackView.currentItem, "SettingsView")
|
||||
stackView.currentItem instanceof SettingsView
|
||||
|
||||
iconSource: "qrc:/res/discord.svg"
|
||||
|
||||
@@ -304,7 +316,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: qmltypeof(stackView.currentItem, "PcView")
|
||||
visible: stackView.currentItem instanceof PcView
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
@@ -402,7 +414,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", GamepadMapper)
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -414,7 +426,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", SettingsView)
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
|
||||
@@ -51,7 +51,11 @@ void SdlGamepadKeyNavigation::enable()
|
||||
// on first init of the GC subsystem. We can't depend on them due to
|
||||
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
||||
// will attach ourselves.
|
||||
SDL_PumpEvents();
|
||||
//
|
||||
// NB: We use SDL_JoystickUpdate() instead of SDL_PumpEvents() because
|
||||
// the latter can do a bit more work that we want (like handling video
|
||||
// events that we intentionally do not want to process yet).
|
||||
SDL_JoystickUpdate();
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||
|
||||
// Open all currently attached game controllers
|
||||
@@ -99,17 +103,19 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
|
||||
// Update joystick state without pumping other events (see enable() comment)
|
||||
SDL_JoystickUpdate();
|
||||
|
||||
// Discard any pending button events on the first poll to avoid picking up
|
||||
// stale input data from the stream session (like the quit combo).
|
||||
if (m_FirstPoll) {
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
|
||||
|
||||
m_FirstPoll = false;
|
||||
}
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
// Peep events rather than polling to avoid calling SDL_PumpEvents()
|
||||
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
// SDL may send us a quit event since we initialize
|
||||
@@ -212,7 +218,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
}
|
||||
|
||||
// Handle analog sticks by polling
|
||||
for (auto gc : m_Gamepads) {
|
||||
for (auto gc : std::as_const(m_Gamepads)) {
|
||||
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
|
||||
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
|
||||
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+260
-245
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+246
-231
File diff suppressed because it is too large
Load Diff
+234
-219
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.
+234
-219
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+238
-223
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+448
-433
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+244
-229
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+259
-244
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+244
-229
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+456
-441
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+404
-389
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+244
-229
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+478
-463
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+237
-222
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+360
-345
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+237
-222
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
+234
-219
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+247
-232
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+269
-254
File diff suppressed because it is too large
Load Diff
+298
-49
@@ -13,6 +13,11 @@
|
||||
#include <QTemporaryFile>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <sys/socket.h>
|
||||
#include <signal.h>
|
||||
#endif
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
// that might include SDL.h themselves.
|
||||
@@ -28,6 +33,7 @@
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <dxgi1_6.h>
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include <openssl/ssl.h>
|
||||
#endif
|
||||
@@ -60,9 +66,16 @@
|
||||
// Log to console for debug Mac builds
|
||||
#endif
|
||||
|
||||
// StreamUtils::setAsyncLogging() exposes control of this to the Session
|
||||
// class to enable async logging once the stream has started.
|
||||
//
|
||||
// FIXME: Clean this up
|
||||
QAtomicInt g_AsyncLoggingEnabled;
|
||||
|
||||
static QElapsedTimer s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stderr);
|
||||
static QThreadPool s_LoggerThread;
|
||||
static QMutex s_SyncLoggerMutex;
|
||||
static bool s_SuppressVerboseOutput;
|
||||
static QRegularExpression k_RikeyRegex("&rikey=\\w+");
|
||||
static QRegularExpression k_RikeyIdRegex("&rikeyid=[\\d-]+");
|
||||
@@ -73,6 +86,10 @@ static QAtomicInteger<uint64_t> s_LogBytesWritten = 0;
|
||||
static QFile* s_LoggerFile;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_DRM_MASTER_HOOKS
|
||||
extern "C" bool g_DisableDrmHooks;
|
||||
#endif
|
||||
|
||||
class LoggerTask : public QRunnable
|
||||
{
|
||||
public:
|
||||
@@ -83,6 +100,11 @@ public:
|
||||
|
||||
void run() override
|
||||
{
|
||||
// QTextStream is not thread-safe, so we must lock. This will generally
|
||||
// only contend in synchronous logging mode or during a transition
|
||||
// between synchronous and asynchronous. Asynchronous won't contend in
|
||||
// the common case because we only have a single logging thread.
|
||||
QMutexLocker locker(&s_SyncLoggerMutex);
|
||||
s_LoggerStream << m_Msg;
|
||||
s_LoggerStream.flush();
|
||||
}
|
||||
@@ -96,7 +118,7 @@ void logToLoggerStream(QString& message)
|
||||
#if defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||
// Output log messages to a debugger if attached
|
||||
if (IsDebuggerPresent()) {
|
||||
static QString lineBuffer;
|
||||
thread_local QString lineBuffer;
|
||||
lineBuffer += message;
|
||||
if (message.endsWith('\n')) {
|
||||
OutputDebugStringW(lineBuffer.toStdWString().c_str());
|
||||
@@ -115,20 +137,19 @@ void logToLoggerStream(QString& message)
|
||||
return;
|
||||
}
|
||||
else if (oldLogSize >= k_MaxLogSizeBytes - message.size()) {
|
||||
s_LoggerThread.waitForDone();
|
||||
s_LoggerStream << "Log size limit reached!";
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
s_LoggerStream << Qt::endl;
|
||||
#else
|
||||
s_LoggerStream << endl;
|
||||
#endif
|
||||
s_LoggerStream.flush();
|
||||
return;
|
||||
// Write one final message
|
||||
message = "Log size limit reached!";
|
||||
}
|
||||
#endif
|
||||
|
||||
// Queue the log message to be written asynchronously
|
||||
s_LoggerThread.start(new LoggerTask(message));
|
||||
if (g_AsyncLoggingEnabled) {
|
||||
// Queue the log message to be written asynchronously
|
||||
s_LoggerThread.start(new LoggerTask(message));
|
||||
}
|
||||
else {
|
||||
// Log the message immediately
|
||||
LoggerTask(message).run();
|
||||
}
|
||||
}
|
||||
|
||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||
@@ -298,12 +319,104 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||
}
|
||||
|
||||
// Sleep for a moment to allow the logging thread to finish up before crashing
|
||||
if (g_AsyncLoggingEnabled) {
|
||||
Sleep(500);
|
||||
}
|
||||
|
||||
// Let the program crash and WER collect a dump
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
static int signalFds[2];
|
||||
|
||||
void handleSignal(int sig)
|
||||
{
|
||||
send(signalFds[0], &sig, sizeof(sig), 0);
|
||||
}
|
||||
|
||||
int SDLCALL signalHandlerThread(void* data)
|
||||
{
|
||||
Q_UNUSED(data);
|
||||
|
||||
Session* lastSession = nullptr;
|
||||
bool requestedQuit = false;
|
||||
|
||||
int sig;
|
||||
while (recv(signalFds[1], &sig, sizeof(sig), MSG_WAITALL) == sizeof(sig)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Received signal: %d", sig);
|
||||
|
||||
Session* session;
|
||||
switch (sig) {
|
||||
case SIGINT:
|
||||
case SIGTERM:
|
||||
// Check if we have an active streaming session
|
||||
session = Session::get();
|
||||
if (session != nullptr) {
|
||||
// Exit immediately if we haven't changed state since last attempt
|
||||
if (session == lastSession || requestedQuit) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Exiting immediately on second signal");
|
||||
_Exit(1);
|
||||
}
|
||||
|
||||
if (sig == SIGTERM) {
|
||||
// If this is a SIGTERM, set the flag to quit
|
||||
session->setShouldExit();
|
||||
requestedQuit = true;
|
||||
}
|
||||
|
||||
// Stop the streaming session
|
||||
session->interrupt();
|
||||
lastSession = session;
|
||||
}
|
||||
else {
|
||||
// Exit immediately if we haven't changed state since last attempt
|
||||
if (requestedQuit) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Exiting immediately on second signal");
|
||||
_Exit(1);
|
||||
}
|
||||
|
||||
// If we're not streaming, we'll close the whole app
|
||||
QCoreApplication::instance()->quit();
|
||||
requestedQuit = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void configureSignalHandlers()
|
||||
{
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, signalFds) == -1) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"socketpair() failed: %d",
|
||||
errno);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a thread to handle our signals safely outside of signal context
|
||||
SDL_Thread* thread = SDL_CreateThread(signalHandlerThread, "Signal Handler", nullptr);
|
||||
SDL_DetachThread(thread);
|
||||
|
||||
struct sigaction sa = {};
|
||||
sa.sa_handler = handleSignal;
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
SDL_SetMainReady();
|
||||
@@ -360,11 +473,18 @@ int main(int argc, char *argv[])
|
||||
|
||||
// Serialize log messages on a single thread
|
||||
s_LoggerThread.setMaxThreadCount(1);
|
||||
|
||||
s_LoggerTime.start();
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
|
||||
// Register our logger with all libraries
|
||||
#if SDL_VERSION_ATLEAST(3, 0, 0)
|
||||
SDL_SetLogOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
#else
|
||||
SDL_LogOutputFunction oldSdlLogFn;
|
||||
void* oldSdlLogUserdata;
|
||||
SDL_LogGetOutputFunction(&oldSdlLogFn, &oldSdlLogUserdata);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
#endif
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
#ifdef HAVE_FFMPEG
|
||||
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||
#endif
|
||||
@@ -387,7 +507,7 @@ int main(int argc, char *argv[])
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll on Win32 platforms by calling a dummy function.
|
||||
AntiHookingDummyImport();
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#elif defined(APP_IMAGE)
|
||||
// Force libssl.so to be directly linked to our binary, so
|
||||
// linuxdeployqt can find it and include it in our AppImage.
|
||||
// QtNetwork will pull it in via dlopen().
|
||||
@@ -395,9 +515,9 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
// We keep this at function scope to ensure it stays around while we're running,
|
||||
// becaue the Qt QPA will need to read it. Since the temporary file is only
|
||||
// because the Qt QPA will need to read it. Since the temporary file is only
|
||||
// created when open() is called, this doesn't do any harm for other platforms.
|
||||
QTemporaryFile eglfsConfigFile("eglfs_override_XXXXXX.conf");
|
||||
QTemporaryFile eglfsConfigFile;
|
||||
|
||||
// Avoid using High DPI on EGLFS. It breaks font rendering.
|
||||
// https://bugreports.qt.io/browse/QTBUG-64377
|
||||
@@ -440,6 +560,7 @@ int main(int argc, char *argv[])
|
||||
qInfo() << "Overriding default Qt EGLFS card selection to" << cardOverride;
|
||||
QTextStream(&eglfsConfigFile) << "{ \"device\": \"" << cardOverride << "\" }";
|
||||
qputenv("QT_QPA_EGLFS_KMS_CONFIG", eglfsConfigFile.fileName().toUtf8());
|
||||
eglfsConfigFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,11 +573,38 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef Q_PROCESSOR_X86
|
||||
bool forceGles;
|
||||
if (!Utils::getEnvironmentVariableOverride("FORCE_QT_GLES", &forceGles)) {
|
||||
forceGles = WMUtils::isRunningNvidiaProprietaryDriverX11() ||
|
||||
!WMUtils::supportsDesktopGLWithEGL();
|
||||
}
|
||||
if (forceGles) {
|
||||
// The Nvidia proprietary driver causes Qt to render a black window when using
|
||||
// the default Desktop GL profile with EGL. AS a workaround, we default to
|
||||
// OpenGL ES when running on Nvidia on X11.
|
||||
// https://qt-project.atlassian.net/browse/QTBUG-106065
|
||||
QSurfaceFormat fmt;
|
||||
fmt.setRenderableType(QSurfaceFormat::OpenGLES);
|
||||
QSurfaceFormat::setDefaultFormat(fmt);
|
||||
}
|
||||
|
||||
// Some ARM and RISC-V embedded devices don't have working GLX which can cause
|
||||
// SDL to fail to find a working OpenGL implementation at all. Let's force EGL
|
||||
// on non-x86 platforms, since GLX is deprecated anyway.
|
||||
// on all platforms for both SDL and Qt. This also avoids GLX-EGL interop issues
|
||||
// when trying to use EGL on the main thread after Qt uses GLX.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1");
|
||||
qputenv("QT_XCB_GL_INTEGRATION", "xcb_egl");
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Let us see the true VBlank rather than DWM's approximation. We do this here
|
||||
// because this API must be called before the first swapchain (which Qt will
|
||||
// create when the window is displayed). This is supported on Win11 22H2+.
|
||||
auto fnDXGIDisableVBlankVirtualization =
|
||||
(decltype(DXGIDisableVBlankVirtualization)*)GetProcAddress(GetModuleHandleW(L"dxgi.dll"),
|
||||
"DXGIDisableVBlankVirtualization");
|
||||
if (fnDXGIDisableVBlankVirtualization) {
|
||||
fnDXGIDisableVBlankVirtualization();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
@@ -522,9 +670,13 @@ int main(int argc, char *argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
#if defined(STEAM_LINK) || defined(Q_OS_WIN32)
|
||||
// Steam Link requires that we initialize video before creating our
|
||||
// QGuiApplication in order to configure the framebuffer correctly.
|
||||
//
|
||||
// We keep the video subsystem initialized on Windows because it's
|
||||
// much more costly to reinitialize than other platforms. It hurts
|
||||
// the settings page transition performance significantly.
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
@@ -578,13 +730,64 @@ int main(int argc, char *argv[])
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
|
||||
#endif
|
||||
|
||||
// Enable fast parameter checks on SDL 3.4.0+. We don't abuse the API by passing
|
||||
// incorrect objects, so we don't need additional expensive parameter checks.
|
||||
SDL_SetHint("SDL_INVALID_PARAM_CHECKS", "1");
|
||||
|
||||
// Disable hotplug detection for SDL_GetKeyboards() and SDL_GetMice(). We don't
|
||||
// use this functionality and it can cause hangs when querying broken devices.
|
||||
SDL_SetHint("SDL_WINDOWS_DETECT_DEVICE_HOTPLUG", "0");
|
||||
|
||||
// SDL3 supports offloading scaling to the Wayland compositor, which we take
|
||||
// advantage of in the GL_IS_SLOW case to help fillrate-limited GPUs. To stay
|
||||
// consistent with our own scaling logic, we need aspect ratio scaling which
|
||||
// KDE doesn't currently handle properly. As a compromise, we'll just enable
|
||||
// aspect ratio scaling in non-KDE environments.
|
||||
//
|
||||
// NB: We do not force SDL_VIDEO_WAYLAND_MODE_SCALING to "stretch" on KDE,
|
||||
// because SDL 3.6 has a workaround for KDE and switches the default to
|
||||
// "aspect" for all desktops.
|
||||
if (qgetenv("XDG_CURRENT_DESKTOP") != "KDE") {
|
||||
SDL_SetHint("SDL_VIDEO_WAYLAND_MODE_SCALING", "aspect");
|
||||
}
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Force use of the KMSDRM backend for SDL when using Qt platform plugins
|
||||
// that directly draw to the display without a windowing system.
|
||||
if (QGuiApplication::platformName() == "eglfs" || QGuiApplication::platformName() == "linuxfb") {
|
||||
qputenv("SDL_VIDEODRIVER", "kmsdrm");
|
||||
#ifdef Q_OS_UNIX
|
||||
// Register signal handlers to arbitrate between SDL and Qt.
|
||||
// NB: This has to be done after the QGuiApplication is constructed to
|
||||
// ensure Qt has already installed its VT signals before we override
|
||||
// some of them with our own.
|
||||
configureSignalHandlers();
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// If we don't have stdout or stderr handles (which will normally be the case
|
||||
// since we're a /SUBSYSTEM:WINDOWS app), attach to our parent console and use
|
||||
// that for stdout and stderr.
|
||||
//
|
||||
// If we do have stdout or stderr handles, that means the user has used standard
|
||||
// handle redirection. In that case, we don't want to override those handles.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
// If we didn't have an old stdout/stderr handle, use the new CONOUT$ handle
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConOut)) {
|
||||
FILE* fp;
|
||||
if (freopen_s(&fp, "CONOUT$", "w", stdout) == 0) {
|
||||
setvbuf(fp, NULL, _IONBF, 0);
|
||||
}
|
||||
else {
|
||||
freopen_s(&fp, "NUL", "w", stdout);
|
||||
}
|
||||
}
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConErr)) {
|
||||
FILE* fp;
|
||||
if (freopen_s(&fp, "CONOUT$", "w", stderr) == 0) {
|
||||
setvbuf(fp, NULL, _IONBF, 0);
|
||||
}
|
||||
else {
|
||||
freopen_s(&fp, "NUL", "w", stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -594,25 +797,6 @@ int main(int argc, char *argv[])
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
// Don't log to the console since it will jumble the command output
|
||||
s_SuppressVerboseOutput = true;
|
||||
#ifdef Q_OS_WIN32
|
||||
// If we don't have stdout or stderr handles (which will normally be the case
|
||||
// since we're a /SUBSYSTEM:WINDOWS app), attach to our parent console and use
|
||||
// that for stdout and stderr.
|
||||
//
|
||||
// If we do have stdout or stderr handles, that means the user has used standard
|
||||
// handle redirection. In that case, we don't want to override those handles.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
// If we didn't have an old stdout/stderr handle, use the new CONOUT$ handle
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConOut)) {
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConErr)) {
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -630,6 +814,36 @@ int main(int argc, char *argv[])
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// If we're running under sdl2-compat, it may tell us the underlying SDL3 version
|
||||
const char* sdl3Version = SDL_GetHint("SDL3_VERSION");
|
||||
int sdl3VersionInt = 0;
|
||||
if (sdl3Version) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL3 version: %s",
|
||||
sdl3Version);
|
||||
|
||||
// Parse the version into integer form
|
||||
QStringList list = QString(sdl3Version).split('.');
|
||||
Q_ASSERT(list.size() == 3);
|
||||
if (list.size() == 3) {
|
||||
sdl3VersionInt = SDL_VERSIONNUM(list.at(0).toInt(), list.at(1).toInt(), list.at(2).toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// SDL 3.4.0 and 3.4.2 have bugs in atomic KMSDRM support that break us,
|
||||
// so disable atomic on the affected SDL3 versions. Since not all versions
|
||||
// of sdl2-compat will set the SDL3_VERSION hint, we assume that versions
|
||||
// prior to 2.32.66 are affected (since that was released at the same time
|
||||
// as SDL 3.4.4 with the atomic fixes).
|
||||
if ((sdl3VersionInt != 0 && sdl3VersionInt < SDL_VERSIONNUM(3, 4, 4)) ||
|
||||
(runtimeVersion.patch >= 50 && runtimeVersion.patch < 66)) {
|
||||
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Setting SDL_KMSDRM_ATOMIC=0 for older sdl2-compat/SDL3 version");
|
||||
SDL_SetHint("SDL_KMSDRM_ATOMIC", "0");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences::get()->retranslate();
|
||||
|
||||
@@ -642,15 +856,29 @@ int main(int argc, char *argv[])
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||
if (QGuiApplication::platformName() == "xcb") {
|
||||
if (WMUtils::isRunningWayland()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||
}
|
||||
qputenv("SDL_VIDEODRIVER", "x11");
|
||||
}
|
||||
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||
qputenv("SDL_VIDEODRIVER", "wayland");
|
||||
}
|
||||
#ifndef STEAM_LINK
|
||||
// Force use of the KMSDRM backend for SDL when using Qt platform plugins
|
||||
// that directly draw to the display without a windowing system.
|
||||
else if (QGuiApplication::platformName() == "eglfs" || QGuiApplication::platformName() == "linuxfb") {
|
||||
qputenv("SDL_VIDEODRIVER", "kmsdrm");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_DRM_MASTER_HOOKS
|
||||
// Only use the Qt-SDL DRM master interoperability hooks if Qt is using KMS
|
||||
g_DisableDrmHooks = QGuiApplication::platformName() != "eglfs";
|
||||
#endif
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
|
||||
@@ -686,7 +914,7 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
@@ -736,6 +964,12 @@ int main(int argc, char *argv[])
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_PRIMARY")) {
|
||||
// Qt 6.9 began to use a different shade of Material.Indigo when we use a dark theme
|
||||
// (which is all the time). The new color looks washed out, so manually specify the
|
||||
// old primary color unless the user overrides it themselves.
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_PRIMARY", "#3F51B5");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
@@ -788,6 +1022,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
if (hasGUI) {
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
engine.rootContext()->setContextProperty("runConfigChecks", commandLineParserResult == GlobalCommandLineParser::NormalStartRequested);
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
@@ -801,6 +1036,20 @@ int main(int argc, char *argv[])
|
||||
// sometimes freezing and blocking process exit.
|
||||
QThreadPool::globalInstance()->waitForDone(30000);
|
||||
|
||||
// Restore the default logger for all libraries before shutting down ours
|
||||
#if SDL_VERSION_ATLEAST(3, 0, 0)
|
||||
SDL_SetLogOutputFunction(SDL_GetDefaultLogOutputFunction(), nullptr);
|
||||
#else
|
||||
SDL_LogSetOutputFunction(oldSdlLogFn, oldSdlLogUserdata);
|
||||
#endif
|
||||
qInstallMessageHandler(nullptr);
|
||||
#ifdef HAVE_FFMPEG
|
||||
av_log_set_callback(av_log_default_callback);
|
||||
#endif
|
||||
|
||||
// We should not be in async logging mode anymore
|
||||
Q_ASSERT(g_AsyncLoggingEnabled == 0);
|
||||
|
||||
// Wait for pending log messages to be printed
|
||||
s_LoggerThread.waitForDone();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user