Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821d4ac9dd | ||
|
|
f4408473e6 | ||
|
|
1f759577ce | ||
|
|
04376ca047 | ||
|
|
7ac108f495 | ||
|
|
a2eece0f5c | ||
|
|
a03279df3b | ||
|
|
bef6c2b665 | ||
|
|
8002c65693 | ||
|
|
5c7247b8b5 | ||
|
|
1a18e68441 | ||
|
|
72afa55897 | ||
|
|
eed9fca10c | ||
|
|
25d97e187a | ||
|
|
7ca2ca8045 | ||
|
|
54885c3e5f | ||
|
|
b8ffb6e53d | ||
|
|
7bc43c226d | ||
|
|
da4697794d | ||
|
|
879c524658 | ||
|
|
e3a8f010c3 | ||
|
|
f2f8f92172 | ||
|
|
265ec96c6f | ||
|
|
6e06174db6 | ||
|
|
9677569293 | ||
|
|
c8e00195ed | ||
|
|
64bc189010 | ||
|
|
12496e4432 | ||
|
|
9e2fd67487 | ||
|
|
af17d56cbd | ||
|
|
6e0a657b76 | ||
|
|
b0a9fc7085 | ||
|
|
5e47b8a835 | ||
|
|
0b94ad1fef | ||
|
|
9dcd856099 | ||
|
|
cfbcc4093c | ||
|
|
b8169ae08b | ||
|
|
103625fbdd | ||
|
|
4712080a1b | ||
|
|
93c683be3a | ||
|
|
c9a7c15f98 | ||
|
|
64a08f0533 | ||
|
|
52c6455ca5 | ||
|
|
238681d72f | ||
|
|
dbcc6feb3f | ||
|
|
300b53f28c | ||
|
|
3dabb1c891 | ||
|
|
4381f7a973 | ||
|
|
77b767ae5b | ||
|
|
2c4f78133f | ||
|
|
ec2c181f4e | ||
|
|
fae98eb13a | ||
|
|
f1d117d5d7 | ||
|
|
529ac8041a | ||
|
|
391e84e0f4 | ||
|
|
6a2b717ff1 | ||
|
|
475a39cb73 | ||
|
|
4dba74e2f9 | ||
|
|
587d783955 | ||
|
|
3bfe87d516 | ||
|
|
d3e43012fb | ||
|
|
5da156b433 | ||
|
|
b2eeb63b6f | ||
|
|
a7517dace3 | ||
|
|
bd9f2d2009 | ||
|
|
78730f7235 | ||
|
|
8a2491d955 | ||
|
|
0d68faf40c | ||
|
|
957a2b2169 | ||
|
|
856318f947 | ||
|
|
4df9d6b20a | ||
|
|
8519d09141 | ||
|
|
1b4fe83739 | ||
|
|
8fc825b2eb | ||
|
|
6d0fc22ab5 | ||
|
|
9a71b4c237 | ||
|
|
9aa487418f | ||
|
|
203b2fcaca | ||
|
|
dae21f2638 | ||
|
|
af6e99cbac | ||
|
|
6d6acf4308 | ||
|
|
7dd4815edf | ||
|
|
2c068a99a3 | ||
|
|
fa93364ddd | ||
|
|
9b1eef3f63 | ||
|
|
ed6c07fd4b | ||
|
|
5ad96f5287 | ||
|
|
fa8845c7e2 | ||
|
|
b96ace7f04 | ||
|
|
234a8dc3d5 | ||
|
|
0467c2f893 | ||
|
|
762fe92933 | ||
|
|
16d105d462 | ||
|
|
8a8b624f47 | ||
|
|
9470615c63 | ||
|
|
8f0ea700a5 | ||
|
|
b7cbec0b1c | ||
|
|
9be9934b8c | ||
|
|
55f0e1e1d5 | ||
|
|
bbd784fc85 | ||
|
|
408f93732b | ||
|
|
fb53c95207 | ||
|
|
960e083152 | ||
|
|
7e560aa09e | ||
|
|
7d9478b774 | ||
|
|
0fa0ab43c9 | ||
|
|
f9cdd49d2e | ||
|
|
7661f5a3d7 | ||
|
|
395f337bbb | ||
|
|
9e9fe2a2cf | ||
|
|
5bf88cfe96 | ||
|
|
f7d3c10c9d | ||
|
|
6b395c816f | ||
|
|
d6e7173af0 | ||
|
|
1a60484abc | ||
|
|
62f765b2b4 | ||
|
|
57f2fb07f0 | ||
|
|
1b8c3aae73 | ||
|
|
845e84adb7 | ||
|
|
91c0429e2c | ||
|
|
30f673efe1 | ||
|
|
c1b1719914 | ||
|
|
9fbdd794b9 | ||
|
|
38ff2bf5cb | ||
|
|
345e800abd | ||
|
|
404eaa44e4 | ||
|
|
8e257ab241 | ||
|
|
fabbad5370 | ||
|
|
7d61acb2a8 | ||
|
|
f6a451d6e7 | ||
|
|
e68a15c825 | ||
|
|
f929cffce7 | ||
|
|
f171588616 | ||
|
|
e467ce7ce8 | ||
|
|
1fda7550da | ||
|
|
f714a5d0cb | ||
|
|
2a3d5a27a8 | ||
|
|
c9bf5ae79d | ||
|
|
cf2d8f71d6 | ||
|
|
611d9ac1ba | ||
|
|
f3309087ab | ||
|
|
860d317330 | ||
|
|
bab11a731e | ||
|
|
df9e7bead0 | ||
|
|
962dc847e7 | ||
|
|
566181e8d7 | ||
|
|
5f5f835b2e | ||
|
|
c0e697eb58 | ||
|
|
d3c2f50b20 | ||
|
|
f1ee1eeb63 | ||
|
|
bb18519e44 | ||
|
|
8e2ad133ea | ||
|
|
5e82e8413f | ||
|
|
917f6e5ee9 | ||
|
|
e1ca6e644e | ||
|
|
f1e89242cb | ||
|
|
91145cde0f | ||
|
|
8ae01fae3f | ||
|
|
4e60ccb2ff | ||
|
|
186052f21d | ||
|
|
0fb3bb0727 | ||
|
|
8f61a11452 | ||
|
|
4b1d3b0125 | ||
|
|
3f81d055af | ||
|
|
c61182bcb2 | ||
|
|
2fdfb3e998 | ||
|
|
9beb51ecac | ||
|
|
7be167856e | ||
|
|
e1d68ef097 | ||
|
|
5ec256a734 | ||
|
|
e4d7d190e2 | ||
|
|
b6763bb9f0 | ||
|
|
6d9c0c96c9 | ||
|
|
73dd5630df | ||
|
|
b76645e26b | ||
|
|
9cc20c27ab | ||
|
|
36b42f6e50 | ||
|
|
45ebf2ca7d | ||
|
|
2140c94cd1 | ||
|
|
470769e910 | ||
|
|
493165f57f | ||
|
|
1dcf55515b | ||
|
|
b04bc5117d | ||
|
|
5cbb38091b | ||
|
|
b076744f00 | ||
|
|
723e815546 | ||
|
|
ca72cbb135 | ||
|
|
1e8c2b179a | ||
|
|
f2ed3a8502 | ||
|
|
2bcc15e1d2 | ||
|
|
4935baa72a | ||
|
|
10cfaa2a5f | ||
|
|
3117e360f4 | ||
|
|
48a770edec | ||
|
|
fca402138f | ||
|
|
2853e498b0 | ||
|
|
509dba2264 | ||
|
|
b8dd131af8 | ||
|
|
0c18bcdd5e | ||
|
|
624578f286 | ||
|
|
c18e99c9ec | ||
|
|
979ba2dab7 | ||
|
|
56277af5d1 | ||
|
|
608db6a3e5 | ||
|
|
be52272e5c | ||
|
|
32f305347f | ||
|
|
18dd6b09bb | ||
|
|
3ed5f9edf7 | ||
|
|
017362a5d1 | ||
|
|
e250e08242 | ||
|
|
48d5a7fb51 | ||
|
|
141ee11e2f | ||
|
|
4e82ea298e | ||
|
|
22be912788 | ||
|
|
02242b9b34 | ||
|
|
e9c1f883da | ||
|
|
141f6669e8 | ||
|
|
6300d242cc | ||
|
|
186579266c | ||
|
|
13490af90c | ||
|
|
23a9c2cd9e | ||
|
|
88631e4a63 | ||
|
|
9e744ce169 | ||
|
|
77a68db8c1 | ||
|
|
27247f253c | ||
|
|
ada7f92c75 | ||
|
|
87db4e88a5 | ||
|
|
277594139b | ||
|
|
79d5e35306 | ||
|
|
cda80b319e | ||
|
|
ab4b024df4 | ||
|
|
7f9cbac232 | ||
|
|
8470ffcb0d | ||
|
|
92c897762b | ||
|
|
36014cfcfd | ||
|
|
df26f731eb | ||
|
|
e39b5a6a72 | ||
|
|
c7138fe36c | ||
|
|
b5deab772d | ||
|
|
4bfeccec74 | ||
|
|
42ed39a67f | ||
|
|
68612db86d | ||
|
|
3ad5f1478c | ||
|
|
a81b501d7d | ||
|
|
871988b2cb | ||
|
|
59d087adf5 | ||
|
|
d9ed3f9694 | ||
|
|
e652dbcc79 | ||
|
|
b5965d763c | ||
|
|
717c3903d1 | ||
|
|
5854ce2000 | ||
|
|
eb1316ce32 | ||
|
|
c31e7262b2 | ||
|
|
9e6aadffa1 | ||
|
|
0bde932550 | ||
|
|
f5499be215 | ||
|
|
42988e0dbf | ||
|
|
b16aac0c06 | ||
|
|
7c74cdc50b | ||
|
|
26ee8564f8 | ||
|
|
718a575478 | ||
|
|
1f314b839e | ||
|
|
34494e4c89 | ||
|
|
03dc563b32 | ||
|
|
5e4182f0a0 | ||
|
|
010475e8a2 | ||
|
|
bd170c4bd6 | ||
|
|
386e463942 | ||
|
|
9d840afdfb | ||
|
|
83b81aad4b | ||
|
|
4caa7ec749 | ||
|
|
cf3cbd627d | ||
|
|
3bb62a82e1 | ||
|
|
a06a625d48 | ||
|
|
13d686b6f8 | ||
|
|
75c7b2828f | ||
|
|
18c5e19ac9 | ||
|
|
06635d9f9a | ||
|
|
ad57a55d12 | ||
|
|
a97e4babf9 | ||
|
|
1117c49867 | ||
|
|
dc723a8da1 | ||
|
|
27239ff320 | ||
|
|
b073c90f0f | ||
|
|
519626a6d5 | ||
|
|
71c11cacd9 | ||
|
|
b7b452875d | ||
|
|
eb536c2273 | ||
|
|
c816a6198c | ||
|
|
399154f5f5 | ||
|
|
64a839c2f9 | ||
|
|
20288210be | ||
|
|
7b235743cb | ||
|
|
4c507f1179 | ||
|
|
e516861366 | ||
|
|
48c2a8c5b0 | ||
|
|
b41e8c07d2 | ||
|
|
10b5245919 | ||
|
|
86874a2e1c | ||
|
|
f951a01a68 | ||
|
|
d12fed3e7f | ||
|
|
4566139c75 | ||
|
|
38df63b962 | ||
|
|
faaf9dfa07 | ||
|
|
9c2befc386 | ||
|
|
e85ca2a6c9 | ||
|
|
e32aace443 | ||
|
|
bd8e4a4bce | ||
|
|
5ee1358712 | ||
|
|
06501bd4b7 | ||
|
|
a4ad58d9ec | ||
|
|
76d99cd60e | ||
|
|
1b85dcc829 | ||
|
|
c97e298b80 |
No files matched your search
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us address broken functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Steps to reproduce**
|
||||||
|
Any special steps that are required for the bug to appear.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||||
|
|
||||||
|
**Moonlight settings (please complete the following information)**
|
||||||
|
- Have any settings been adjusted from defaults?
|
||||||
|
- If so, which settings have been changed?
|
||||||
|
- Does the problem still occur after reverting settings back to default?
|
||||||
|
|
||||||
|
**Client PC details (please complete the following information)**
|
||||||
|
- OS: [e.g. Windows 10 1803]
|
||||||
|
- Moonlight Version: [e.g. v0.2.0]
|
||||||
|
|
||||||
|
**Logs (please attach)**
|
||||||
|
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||||
|
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||||
|
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||||
|
|
||||||
|
daysUntilClose: 7
|
||||||
|
responseRequiredLabel: 'need more info'
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there was no response to a
|
||||||
|
request for more information from the issue opener. Please leave a comment or
|
||||||
|
open a new issue if you have additional information related to this issue.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||||
|
|
||||||
|
daysUntilStale: 90
|
||||||
|
daysUntilClose: 7
|
||||||
|
exemptLabels:
|
||||||
|
- accepted
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
- meta
|
||||||
|
staleLabel: stale
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
closeComment: false
|
||||||
@@ -1 +1,5 @@
|
|||||||
*pro.user
|
*pro.user
|
||||||
|
|
||||||
|
|
||||||
|
**/.vs/
|
||||||
|
build/
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
[submodule "moonlight-common-c/moonlight-common-c"]
|
[submodule "moonlight-common-c/moonlight-common-c"]
|
||||||
path = moonlight-common-c/moonlight-common-c
|
path = moonlight-common-c/moonlight-common-c
|
||||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||||
[submodule "opus/opus"]
|
|
||||||
path = opus/opus
|
|
||||||
url = https://github.com/xiph/opus.git
|
|
||||||
[submodule "qmdnsengine/qmdnsengine"]
|
[submodule "qmdnsengine/qmdnsengine"]
|
||||||
path = qmdnsengine/qmdnsengine
|
path = qmdnsengine/qmdnsengine
|
||||||
url = https://github.com/cgutman/qmdnsengine.git
|
url = https://github.com/cgutman/qmdnsengine.git
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
language: cpp
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: osx
|
||||||
|
compiler: clang
|
||||||
|
- os: linux
|
||||||
|
sudo: required
|
||||||
|
dist: xenial
|
||||||
|
compiler: gcc
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
||||||
|
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||||
|
packages:
|
||||||
|
- qt59base
|
||||||
|
- qt59quickcontrols2
|
||||||
|
- qt59svg
|
||||||
|
- qt59multimedia
|
||||||
|
- libesd0-dev
|
||||||
|
- libgl-dev
|
||||||
|
- libegl1-mesa-dev
|
||||||
|
- libxkbcommon-dev
|
||||||
|
- wayland-protocols
|
||||||
|
- libopus-dev
|
||||||
|
- libavcodec-dev
|
||||||
|
- libavutil-dev
|
||||||
|
- libva-dev
|
||||||
|
- libvdpau-dev
|
||||||
|
|
||||||
|
install:
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
|
||||||
|
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||||
|
- qmake -v
|
||||||
|
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||||
|
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||||
|
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||||
|
- make debug
|
||||||
|
- make release
|
||||||
@@ -1,20 +1,34 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux.
|
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
This is a work-in-progress port and is only suitable for development use at this point.
|
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
||||||
|
|
||||||
|
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||||
|
|
||||||
|
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||||
|
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
|
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||||
|
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||||
|
- 5.1 surround sound audio
|
||||||
|
- HEVC support for better image quality at reduced bandwidth
|
||||||
|
- Keyboard and mouse support
|
||||||
|
- Gamepad support with SDL gamepad mappings
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||||
3. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
- On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
|
||||||
4. Open the project in Qt Creator or build from qmake on the command line
|
- On Mac, install the latest version of XCode
|
||||||
|
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
|
||||||
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
|
3. Open the project in Qt Creator or build from qmake on the command line
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
|
||||||
|
|
||||||
1. Fork us
|
1. Fork us
|
||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|||||||
@@ -3,24 +3,30 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>moonlight-qt</string>
|
<string>Moonlight</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Created by Qt/QMake</string>
|
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>macos</string>
|
<string>moonlight</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.yourcompany.moonlight-qt</string>
|
<string>com.moonlight-stream.Moonlight</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.10</string>
|
<string>10.11</string>
|
||||||
<key>NOTE</key>
|
|
||||||
<string>This file was generated by Qt/QMake.</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.5.1</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.5.1</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
<description>Moonlight Game Streaming</description>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||||
|
<!-- Windows Vista -->
|
||||||
|
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
</assembly>
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
QT += core quick network quickcontrols2
|
QT += core quick network quickcontrols2 svg multimedia
|
||||||
CONFIG += c++11
|
CONFIG += c++11
|
||||||
|
|
||||||
TARGET = moonlight-qt
|
unix:!macx {
|
||||||
|
TARGET = moonlight
|
||||||
|
} else {
|
||||||
|
# On macOS, this is the name displayed in the global menu bar
|
||||||
|
TARGET = Moonlight
|
||||||
|
}
|
||||||
|
|
||||||
|
# Support debug and release builds from command line for CI
|
||||||
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
# The following define makes your compiler emit warnings if you use
|
||||||
@@ -25,23 +37,44 @@ win32 {
|
|||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
}
|
}
|
||||||
|
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
LIBS += -L$$PWD/../libs/mac/lib
|
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 libavcodec libavdevice libavformat libavutil
|
PKGCONFIG += openssl sdl2 opus
|
||||||
|
|
||||||
|
packagesExist(libavcodec) {
|
||||||
|
PKGCONFIG += libavcodec libavutil
|
||||||
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
|
packagesExist(libva) {
|
||||||
|
packagesExist(libva-x11) {
|
||||||
|
CONFIG += libva-x11
|
||||||
|
}
|
||||||
|
packagesExist(libva-wayland) {
|
||||||
|
CONFIG += libva-wayland
|
||||||
|
}
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
|
|
||||||
|
packagesExist(vdpau) {
|
||||||
|
CONFIG += libvdpau
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavdevice -lavformat -lavutil
|
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
||||||
|
CONFIG += ffmpeg
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavdevice.58 -lavformat.58 -lavutil.56
|
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||||
|
CONFIG += ffmpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -54,18 +87,14 @@ SOURCES += \
|
|||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input.cpp \
|
streaming/input.cpp \
|
||||||
streaming/session.cpp \
|
streaming/session.cpp \
|
||||||
streaming/audio.cpp \
|
streaming/audio/audio.cpp \
|
||||||
streaming/video/ffmpeg.cpp \
|
streaming/audio/renderers/sdlaud.cpp \
|
||||||
|
streaming/audio/renderers/qtaud.cpp \
|
||||||
gui/computermodel.cpp \
|
gui/computermodel.cpp \
|
||||||
gui/appmodel.cpp \
|
gui/appmodel.cpp \
|
||||||
streaming/video/ffmpeg-renderers/sdl.cpp
|
streaming/streamutils.cpp \
|
||||||
|
backend/autoupdatechecker.cpp \
|
||||||
win32 {
|
path.cpp
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
|
||||||
}
|
|
||||||
macx {
|
|
||||||
SOURCES += streaming/video/avfoundation.mm
|
|
||||||
}
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
@@ -77,17 +106,90 @@ HEADERS += \
|
|||||||
settings/streamingpreferences.h \
|
settings/streamingpreferences.h \
|
||||||
streaming/input.hpp \
|
streaming/input.hpp \
|
||||||
streaming/session.hpp \
|
streaming/session.hpp \
|
||||||
|
streaming/audio/renderers/renderer.h \
|
||||||
|
streaming/audio/renderers/sdl.h \
|
||||||
|
streaming/audio/renderers/qtaud.h \
|
||||||
gui/computermodel.h \
|
gui/computermodel.h \
|
||||||
gui/appmodel.h \
|
gui/appmodel.h \
|
||||||
streaming/video/decoder.h \
|
streaming/video/decoder.h \
|
||||||
streaming/video/ffmpeg.h \
|
streaming/streamutils.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h
|
backend/autoupdatechecker.h \
|
||||||
|
path.h
|
||||||
|
|
||||||
|
# Platform-specific renderers and decoders
|
||||||
|
ffmpeg {
|
||||||
|
message(FFmpeg decoder selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_FFMPEG
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||||
|
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg.h \
|
||||||
|
streaming/video/ffmpeg-renderers/renderer.h \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||||
|
}
|
||||||
|
libva {
|
||||||
|
message(VAAPI renderer selected)
|
||||||
|
|
||||||
|
PKGCONFIG += libva
|
||||||
|
DEFINES += HAVE_LIBVA
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
|
||||||
|
}
|
||||||
|
libva-x11 {
|
||||||
|
message(VAAPI X11 support enabled)
|
||||||
|
|
||||||
|
PKGCONFIG += libva-x11
|
||||||
|
DEFINES += HAVE_LIBVA_X11
|
||||||
|
}
|
||||||
|
libva-wayland {
|
||||||
|
message(VAAPI Wayland support enabled)
|
||||||
|
|
||||||
|
PKGCONFIG += libva-wayland
|
||||||
|
DEFINES += HAVE_LIBVA_WAYLAND
|
||||||
|
}
|
||||||
|
libvdpau {
|
||||||
|
message(VDPAU renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_LIBVDPAU
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
||||||
|
}
|
||||||
|
config_SLVideo {
|
||||||
|
message(SLVideo decoder/renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_SLVIDEO
|
||||||
|
LIBS += -lSLVideo
|
||||||
|
SOURCES += streaming/video/sl.cpp
|
||||||
|
HEADERS += streaming/video/sl.h
|
||||||
|
}
|
||||||
win32 {
|
win32 {
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
message(DXVA2 renderer selected)
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/dxva2.h \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
HEADERS += streaming/video/avfoundation.h
|
message(VideoToolbox renderer selected)
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg-renderers/vt.mm \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/vt.h \
|
||||||
|
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
|
||||||
}
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
@@ -107,13 +209,6 @@ else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
|
|||||||
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
||||||
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../opus/release/ -lopus
|
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../opus/debug/ -lopus
|
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../opus/ -lopus
|
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../opus/opus/include
|
|
||||||
DEPENDPATH += $$PWD/../opus/opus/include
|
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||||
@@ -121,14 +216,49 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
|||||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
|
|
||||||
# Default rules for deployment.
|
unix:!macx: {
|
||||||
qnx: target.path = /tmp/$${TARGET}/bin
|
isEmpty(PREFIX) {
|
||||||
else: unix:!android: target.path = /opt/$${TARGET}/bin
|
PREFIX = /usr/local
|
||||||
!isEmpty(target.path): INSTALLS += target
|
}
|
||||||
|
isEmpty(BINDIR) {
|
||||||
|
BINDIR = bin
|
||||||
|
}
|
||||||
|
isEmpty(DATADIR) {
|
||||||
|
DATADIR = share
|
||||||
|
}
|
||||||
|
|
||||||
|
target.path = $$PREFIX/$$BINDIR/
|
||||||
|
|
||||||
|
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
|
||||||
|
desktop.path = $$PREFIX/$$DATADIR/applications/
|
||||||
|
|
||||||
|
icons.files = res/moonlight.svg
|
||||||
|
icons.path = $$PREFIX/$$DATADIR/icons/hicolor/scalable/apps/
|
||||||
|
|
||||||
|
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||||
|
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||||
|
|
||||||
|
appdata.files = gamecontrollerdb.txt
|
||||||
|
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
||||||
|
|
||||||
|
INSTALLS += target appdata desktop icons appstream
|
||||||
|
}
|
||||||
|
win32 {
|
||||||
|
RC_ICONS = moonlight.ico
|
||||||
|
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
|
||||||
|
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||||
|
QMAKE_TARGET_PRODUCT = Moonlight
|
||||||
|
|
||||||
|
CONFIG -= embed_manifest_exe
|
||||||
|
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||||
|
}
|
||||||
macx {
|
macx {
|
||||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||||
APP_QML_FILES.files = res/macos.icns
|
|
||||||
APP_QML_FILES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.files = moonlight.icns gamecontrollerdb.txt
|
||||||
QMAKE_BUNDLE_DATA += APP_QML_FILES
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VERSION = 0.5.1
|
||||||
|
DEFINES += VERSION_STR=\\\"0.5.1\\\"
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include "autoupdatechecker.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||||
|
QObject(parent),
|
||||||
|
m_Nam(this)
|
||||||
|
{
|
||||||
|
// Never communicate over HTTP
|
||||||
|
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
|
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||||
|
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||||
|
|
||||||
|
QString currentVersion(VERSION_STR);
|
||||||
|
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||||
|
parseStringToVersionQuad(currentVersion, m_CurrentVersionQuad);
|
||||||
|
|
||||||
|
// Should at least have a 1.0-style version number
|
||||||
|
Q_ASSERT(m_CurrentVersionQuad.count() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::start()
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
||||||
|
// We'll get a callback when this is finished
|
||||||
|
QUrl url("https://moonlight-stream.com/updates/qt.json");
|
||||||
|
m_Nam.get(QNetworkRequest(url));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
||||||
|
{
|
||||||
|
QStringList list = string.split('.');
|
||||||
|
for (const QString& component : list) {
|
||||||
|
version.append(component.toInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
QTextStream stream(reply);
|
||||||
|
stream.setCodec("UTF-8");
|
||||||
|
|
||||||
|
// Read all data and queue the reply for deletion
|
||||||
|
QString jsonString = stream.readAll();
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &error);
|
||||||
|
if (jsonDoc.isNull()) {
|
||||||
|
qWarning() << "Update manifest malformed:" << error.errorString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray array = jsonDoc.array();
|
||||||
|
if (array.isEmpty()) {
|
||||||
|
qWarning() << "Update manifest doesn't contain an array";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (QJsonValueRef updateEntry : array) {
|
||||||
|
if (updateEntry.isObject()) {
|
||||||
|
QJsonObject updateObj = updateEntry.toObject();
|
||||||
|
if (!updateObj.contains("platform") ||
|
||||||
|
!updateObj.contains("arch") ||
|
||||||
|
!updateObj.contains("version") ||
|
||||||
|
!updateObj.contains("browser_url")) {
|
||||||
|
qWarning() << "Update manifest entry missing vital field";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updateObj["platform"].isString() ||
|
||||||
|
!updateObj["arch"].isString() ||
|
||||||
|
!updateObj["version"].isString() ||
|
||||||
|
!updateObj["browser_url"].isString()) {
|
||||||
|
qWarning() << "Update manifest entry has unexpected vital field type";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||||
|
updateObj["platform"] == QSysInfo::productType()) {
|
||||||
|
qDebug() << "Found update manifest match for current platform";
|
||||||
|
|
||||||
|
QString latestVersion = updateObj["version"].toString();
|
||||||
|
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||||
|
|
||||||
|
QVector<int> latestVersionQuad;
|
||||||
|
|
||||||
|
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
int latestVer = 0;
|
||||||
|
int currentVer = 0;
|
||||||
|
|
||||||
|
// Treat missing decimal places as 0
|
||||||
|
if (i < latestVersionQuad.count()) {
|
||||||
|
latestVer = latestVersionQuad[i];
|
||||||
|
}
|
||||||
|
if (i < m_CurrentVersionQuad.count()) {
|
||||||
|
currentVer = m_CurrentVersionQuad[i];
|
||||||
|
}
|
||||||
|
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVer < latestVer) {
|
||||||
|
qDebug() << "Update available";
|
||||||
|
|
||||||
|
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (currentVer > latestVer) {
|
||||||
|
qDebug() << "Update manifest version lower than current version";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "Update manifest version equal to current version";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Update manifest contained unrecognized entry:" << updateEntry.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qWarning() << "No entry in update manifest found for current platform: "
|
||||||
|
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Update checking failed with error: " << reply->error();
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
|
||||||
|
class AutoUpdateChecker : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AutoUpdateChecker(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Q_INVOKABLE void start();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void onUpdateAvailable(QString url);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||||
|
|
||||||
|
QVector<int> m_CurrentVersionQuad;
|
||||||
|
QNetworkAccessManager m_Nam;
|
||||||
|
};
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
#include "boxartmanager.h"
|
#include "boxartmanager.h"
|
||||||
|
#include "../path.h"
|
||||||
|
|
||||||
#include <QStandardPaths>
|
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QImageWriter>
|
#include <QImageWriter>
|
||||||
|
|
||||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_BoxArtDir(
|
m_BoxArtDir(Path::getBoxArtCacheDir()),
|
||||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart")
|
m_ThreadPool(this)
|
||||||
{
|
{
|
||||||
|
// 4 is a good balance between fast loading for large
|
||||||
|
// app grids and not crushing GFE with tons of requests
|
||||||
|
// and causing UI jank from constantly stalling to decode
|
||||||
|
// new images.
|
||||||
|
m_ThreadPool.setMaxThreadCount(4);
|
||||||
if (!m_BoxArtDir.exists()) {
|
if (!m_BoxArtDir.exists()) {
|
||||||
m_BoxArtDir.mkpath(".");
|
m_BoxArtDir.mkpath(".");
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
// If we get here, we need to fetch asynchronously.
|
// If we get here, we need to fetch asynchronously.
|
||||||
// Kick off a worker on our thread pool to do just that.
|
// Kick off a worker on our thread pool to do just that.
|
||||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||||
QThreadPool::globalInstance()->start(netLoadTask);
|
m_ThreadPool.start(netLoadTask);
|
||||||
|
|
||||||
// Return the placeholder then we can notify the caller
|
// Return the placeholder then we can notify the caller
|
||||||
// later when the real image is ready.
|
// later when the real image is ready.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ private:
|
|||||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||||
|
|
||||||
QDir m_BoxArtDir;
|
QDir m_BoxArtDir;
|
||||||
|
QThreadPool m_ThreadPool;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "computermanager.h"
|
#include "computermanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
@@ -51,6 +52,8 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->appVersion = nullptr;
|
this->appVersion = nullptr;
|
||||||
this->maxLumaPixelsHEVC = 0;
|
this->maxLumaPixelsHEVC = 0;
|
||||||
this->serverCodecModeSupport = 0;
|
this->serverCodecModeSupport = 0;
|
||||||
|
this->pendingQuit = false;
|
||||||
|
this->gpuModel = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -90,7 +93,7 @@ void NvComputer::sortAppList()
|
|||||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||||
{
|
{
|
||||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||||
if (this->name.isNull()) {
|
if (this->name.isEmpty()) {
|
||||||
this->name = "UNKNOWN";
|
this->name = "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||||
if (!codecSupport.isNull()) {
|
if (!codecSupport.isEmpty()) {
|
||||||
this->serverCodecModeSupport = codecSupport.toInt();
|
this->serverCodecModeSupport = codecSupport.toInt();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -112,7 +115,7 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
if (!maxLumaPixelsHEVC.isNull()) {
|
if (!maxLumaPixelsHEVC.isEmpty()) {
|
||||||
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -133,19 +136,21 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||||
|
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||||
this->activeAddress = address;
|
this->activeAddress = address;
|
||||||
this->state = NvComputer::CS_ONLINE;
|
this->state = NvComputer::CS_ONLINE;
|
||||||
|
this->pendingQuit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::wake()
|
bool NvComputer::wake()
|
||||||
{
|
{
|
||||||
if (state == NvComputer::CS_ONLINE) {
|
if (state == NvComputer::CS_ONLINE) {
|
||||||
qWarning() << name << " is already online";
|
qWarning() << name << "is already online";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (macAddress.isNull()) {
|
if (macAddress.isEmpty()) {
|
||||||
qWarning() << name << " has no MAC address stored";
|
qWarning() << name << "has no MAC address stored";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +178,11 @@ bool NvComputer::wake()
|
|||||||
for (QString& addressString : addressList) {
|
for (QString& addressString : addressList) {
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||||
|
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Try all IP addresses that this string resolves to
|
// Try all IP addresses that this string resolves to
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : hostInfo.addresses()) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
@@ -184,7 +194,7 @@ bool NvComputer::wake()
|
|||||||
// Send to all ports
|
// Send to all ports
|
||||||
for (quint16 port : WOL_PORTS) {
|
for (quint16 port : WOL_PORTS) {
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
qDebug() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,7 +217,7 @@ QVector<QString> NvComputer::uniqueAddresses()
|
|||||||
|
|
||||||
// Prune duplicates (always giving precedence to the first)
|
// Prune duplicates (always giving precedence to the first)
|
||||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||||
if (uniqueAddressList[i].isEmpty() || uniqueAddressList[i].isNull()) {
|
if (uniqueAddressList[i].isEmpty()) {
|
||||||
uniqueAddressList.remove(i);
|
uniqueAddressList.remove(i);
|
||||||
i--;
|
i--;
|
||||||
continue;
|
continue;
|
||||||
@@ -264,6 +274,7 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(gfeVersion);
|
ASSIGN_IF_CHANGED(gfeVersion);
|
||||||
ASSIGN_IF_CHANGED(appVersion);
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
return changed;
|
return changed;
|
||||||
@@ -286,6 +297,56 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
settings.endArray();
|
settings.endArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComputerManager::~ComputerManager()
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Lock);
|
||||||
|
|
||||||
|
// Delete machines that haven't been resolved yet
|
||||||
|
while (!m_PendingResolution.isEmpty()) {
|
||||||
|
MdnsPendingComputer* computer = m_PendingResolution.first();
|
||||||
|
delete computer;
|
||||||
|
m_PendingResolution.removeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the browser to stop discovery
|
||||||
|
delete m_MdnsBrowser;
|
||||||
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||||
|
|
||||||
|
// Interrupt all polling threads
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
i.value()->requestInterruption();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all threads to terminate before destroying
|
||||||
|
// the NvComputer objects.
|
||||||
|
i.toFront();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
|
||||||
|
QThread* thread = i.value();
|
||||||
|
thread->wait();
|
||||||
|
delete thread;
|
||||||
|
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||||
|
|
||||||
|
// Destroy all NvComputer objects
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
delete i.value();
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::saveHosts()
|
void ComputerManager::saveHosts()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
@@ -308,17 +369,24 @@ void ComputerManager::startPolling()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start an MDNS query for GameStream hosts
|
StreamingPreferences prefs;
|
||||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
|
||||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
|
||||||
this, [this](const QMdnsEngine::Service& service) {
|
|
||||||
qDebug() << "Discovered mDNS host: " << service.hostname();
|
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
if (prefs.enableMdns) {
|
||||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
// Start an MDNS query for GameStream hosts
|
||||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
||||||
m_PendingResolution.append(pendingComputer);
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||||
});
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||||
|
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||||
|
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
||||||
|
m_PendingResolution.append(pendingComputer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "mDNS is disabled by user preference";
|
||||||
|
}
|
||||||
|
|
||||||
// Start polling threads for each known host
|
// Start polling threads for each known host
|
||||||
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||||
@@ -331,7 +399,7 @@ void ComputerManager::startPolling()
|
|||||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||||
const QHostAddress& address)
|
const QHostAddress& address)
|
||||||
{
|
{
|
||||||
qDebug() << "Resolved " << computer->hostname() << " to " << address.toString();
|
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
|
||||||
|
|
||||||
addNewHost(address.toString(), true);
|
addNewHost(address.toString(), true);
|
||||||
|
|
||||||
@@ -396,6 +464,15 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
|||||||
QThreadPool::globalInstance()->start(pairing);
|
QThreadPool::globalInstance()->start(pairing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::quitRunningApp(NvComputer* computer)
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&computer->lock);
|
||||||
|
computer->pendingQuit = true;
|
||||||
|
|
||||||
|
PendingQuitTask* quit = new PendingQuitTask(this, computer);
|
||||||
|
QThreadPool::globalInstance()->start(quit);
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::stopPollingAsync()
|
void ComputerManager::stopPollingAsync()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_Lock);
|
QWriteLocker lock(&m_Lock);
|
||||||
@@ -447,6 +524,11 @@ ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|||||||
{
|
{
|
||||||
emit computerStateChanged(computer);
|
emit computerStateChanged(computer);
|
||||||
|
|
||||||
|
if (computer->pendingQuit && computer->currentGameId == 0) {
|
||||||
|
computer->pendingQuit = false;
|
||||||
|
emit quitAppCompleted(QVariant());
|
||||||
|
}
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
// Save updated hosts to QSettings
|
||||||
saveHosts();
|
saveHosts();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,14 @@
|
|||||||
class NvComputer
|
class NvComputer
|
||||||
{
|
{
|
||||||
friend class PcMonitorThread;
|
friend class PcMonitorThread;
|
||||||
|
friend class ComputerManager;
|
||||||
|
friend class PendingQuitTask;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void sortAppList();
|
void sortAppList();
|
||||||
|
|
||||||
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NvComputer(QString address, QString serverInfo);
|
explicit NvComputer(QString address, QString serverInfo);
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ public:
|
|||||||
QVector<NvDisplayMode> displayModes;
|
QVector<NvDisplayMode> displayModes;
|
||||||
int maxLumaPixelsHEVC;
|
int maxLumaPixelsHEVC;
|
||||||
int serverCodecModeSupport;
|
int serverCodecModeSupport;
|
||||||
|
QString gpuModel;
|
||||||
|
|
||||||
// Persisted traits
|
// Persisted traits
|
||||||
QString localAddress;
|
QString localAddress;
|
||||||
@@ -98,7 +103,7 @@ private:
|
|||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo();
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -148,31 +153,31 @@ private:
|
|||||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||||
while (!isInterruptionRequested()) {
|
while (!isInterruptionRequested()) {
|
||||||
bool stateChanged = false;
|
bool stateChanged = false;
|
||||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
|
bool online = false;
|
||||||
|
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
||||||
|
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||||
if (isInterruptionRequested()) {
|
if (isInterruptionRequested()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
if (tryPollComputer(address, stateChanged)) {
|
||||||
|
if (!wasOnline) {
|
||||||
|
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||||
|
}
|
||||||
|
online = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to continue retrying if we're online
|
|
||||||
if (m_Computer->state == NvComputer::CS_ONLINE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we failed after all retry attempts
|
// Check if we failed after all retry attempts
|
||||||
// Note: we don't need to acquire the read lock here,
|
// Note: we don't need to acquire the read lock here,
|
||||||
// because we're on the writing thread.
|
// because we're on the writing thread.
|
||||||
if (m_Computer->state != NvComputer::CS_ONLINE) {
|
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||||
if (m_Computer->state != NvComputer::CS_OFFLINE) {
|
qInfo() << m_Computer->name << "is now offline";
|
||||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||||
@@ -180,6 +185,13 @@ private:
|
|||||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||||
|
// Notify prior to the app list poll since it may take a while, and we don't
|
||||||
|
// want to delay onlining of a machine, especially if we already have a cached list.
|
||||||
|
if (stateChanged) {
|
||||||
|
emit computerStateChanged(m_Computer);
|
||||||
|
stateChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateAppList(stateChanged)) {
|
if (updateAppList(stateChanged)) {
|
||||||
pollsSinceLastAppListFetch = 0;
|
pollsSinceLastAppListFetch = 0;
|
||||||
}
|
}
|
||||||
@@ -249,6 +261,8 @@ class ComputerManager : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ComputerManager(QObject *parent = nullptr);
|
explicit ComputerManager(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
virtual ~ComputerManager();
|
||||||
|
|
||||||
Q_INVOKABLE void startPolling();
|
Q_INVOKABLE void startPolling();
|
||||||
|
|
||||||
Q_INVOKABLE void stopPollingAsync();
|
Q_INVOKABLE void stopPollingAsync();
|
||||||
@@ -257,6 +271,8 @@ public:
|
|||||||
|
|
||||||
void pairHost(NvComputer* computer, QString pin);
|
void pairHost(NvComputer* computer, QString pin);
|
||||||
|
|
||||||
|
void quitRunningApp(NvComputer* computer);
|
||||||
|
|
||||||
QVector<NvComputer*> getComputers();
|
QVector<NvComputer*> getComputers();
|
||||||
|
|
||||||
// computer is deleted inside this call
|
// computer is deleted inside this call
|
||||||
@@ -269,6 +285,8 @@ signals:
|
|||||||
|
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success);
|
||||||
|
|
||||||
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
@@ -336,6 +354,49 @@ private:
|
|||||||
QString m_Pin;
|
QString m_Pin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class PendingQuitTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||||
|
: m_Computer(computer)
|
||||||
|
{
|
||||||
|
connect(this, &PendingQuitTask::quitAppFailed,
|
||||||
|
computerManager, &ComputerManager::quitAppCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void quitAppFailed(QString error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvHTTP http(m_Computer->activeAddress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m_Computer->currentGameId != 0) {
|
||||||
|
http.quitApp();
|
||||||
|
}
|
||||||
|
} catch (const GfeHttpResponseException& e) {
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
m_Computer->pendingQuit = false;
|
||||||
|
}
|
||||||
|
if (e.getStatusCode() == 599) {
|
||||||
|
// 599 is a special code we make a custom message for
|
||||||
|
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||||
|
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit quitAppFailed(e.toQString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
|
|
||||||
class PendingAddTask : public QObject, public QRunnable
|
class PendingAddTask : public QObject, public QRunnable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -362,9 +423,11 @@ private:
|
|||||||
{
|
{
|
||||||
NvHTTP http(m_Address);
|
NvHTTP http(m_Address);
|
||||||
|
|
||||||
|
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo();
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(false);
|
emit computerAddCompleted(false);
|
||||||
@@ -400,6 +463,7 @@ private:
|
|||||||
|
|
||||||
// Tell our client if something changed
|
// Tell our client if something changed
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||||
emit computerStateChanged(existingComputer);
|
emit computerStateChanged(existingComputer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QRandomGenerator64>
|
|
||||||
|
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/rsa.h>
|
#include <openssl/rsa.h>
|
||||||
#include <openssl/bn.h>
|
#include <openssl/bn.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
#define SER_UNIQUEID "uniqueid"
|
#define SER_UNIQUEID "uniqueid"
|
||||||
#define SER_CERT "certificate"
|
#define SER_CERT "certificate"
|
||||||
@@ -101,7 +101,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
settings.setValue(SER_CERT, m_CachedPemCert);
|
settings.setValue(SER_CERT, m_CachedPemCert);
|
||||||
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
||||||
|
|
||||||
qDebug() << "Wrote new identity credentials to settings";
|
qInfo() << "Wrote new identity credentials to settings";
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityManager::IdentityManager()
|
IdentityManager::IdentityManager()
|
||||||
@@ -112,7 +112,7 @@ IdentityManager::IdentityManager()
|
|||||||
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
||||||
|
|
||||||
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
||||||
qDebug() << "No existing credentials found";
|
qInfo() << "No existing credentials found";
|
||||||
createCredentials(settings);
|
createCredentials(settings);
|
||||||
}
|
}
|
||||||
else if (getSslCertificate().isNull()) {
|
else if (getSslCertificate().isNull()) {
|
||||||
@@ -157,10 +157,14 @@ IdentityManager::getSslKey()
|
|||||||
|
|
||||||
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
||||||
// on macOS/iOS to be able to read it.
|
// on macOS/iOS to be able to read it.
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
||||||
|
#else
|
||||||
|
PEM_write_bio_PrivateKey(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
BUF_MEM* mem;
|
BUF_MEM* mem;
|
||||||
BIO_get_mem_ptr(bio, &mem);
|
BIO_get_mem_ptr(bio, &mem);
|
||||||
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
|
||||||
|
|
||||||
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
|
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
|
||||||
|
|
||||||
BIO_free(bio);
|
BIO_free(bio);
|
||||||
@@ -181,20 +185,21 @@ IdentityManager::getSslConfig()
|
|||||||
QString
|
QString
|
||||||
IdentityManager::getUniqueId()
|
IdentityManager::getUniqueId()
|
||||||
{
|
{
|
||||||
if (m_CachedUniqueId.isNull()) {
|
if (m_CachedUniqueId.isEmpty()) {
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
// Load the unique ID from settings
|
// Load the unique ID from settings
|
||||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
if (!m_CachedUniqueId.isEmpty()) {
|
||||||
qDebug() << "Loaded unique ID from settings: " << m_CachedUniqueId;
|
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Generate a new unique ID in base 16
|
// Generate a new unique ID in base 16
|
||||||
m_CachedUniqueId = QString::number(
|
uint64_t uid;
|
||||||
QRandomGenerator64::securelySeeded().generate64(), 16);
|
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
|
||||||
|
m_CachedUniqueId = QString::number(uid, 16);
|
||||||
|
|
||||||
qDebug() << "Generated new unique ID: " << m_CachedUniqueId;
|
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
|
||||||
|
|
||||||
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
NvHTTP::NvHTTP(QString address) :
|
NvHTTP::NvHTTP(QString address) :
|
||||||
m_Address(address)
|
m_Address(address)
|
||||||
{
|
{
|
||||||
|
Q_ASSERT(!address.isEmpty());
|
||||||
|
|
||||||
m_BaseUrlHttp.setScheme("http");
|
m_BaseUrlHttp.setScheme("http");
|
||||||
m_BaseUrlHttps.setScheme("https");
|
m_BaseUrlHttps.setScheme("https");
|
||||||
m_BaseUrlHttp.setHost(address);
|
m_BaseUrlHttp.setHost(address);
|
||||||
@@ -27,9 +29,15 @@ NvHTTP::NvHTTP(QString address) :
|
|||||||
QVector<int>
|
QVector<int>
|
||||||
NvHTTP::parseQuad(QString quad)
|
NvHTTP::parseQuad(QString quad)
|
||||||
{
|
{
|
||||||
QStringList parts = quad.split(".");
|
|
||||||
QVector<int> ret;
|
QVector<int> ret;
|
||||||
|
|
||||||
|
// Return an empty vector for old GFE versions
|
||||||
|
// that were missing GfeVersion.
|
||||||
|
if (quad.isEmpty()) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = quad.split(".");
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
ret.append(parts.at(i).toInt());
|
ret.append(parts.at(i).toInt());
|
||||||
@@ -56,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getServerInfo()
|
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
@@ -67,7 +75,8 @@ NvHTTP::getServerInfo()
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true);
|
true,
|
||||||
|
logLevel);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -79,7 +88,8 @@ NvHTTP::getServerInfo()
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true);
|
true,
|
||||||
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -123,6 +133,11 @@ NvHTTP::launchApp(int appId,
|
|||||||
bool localAudio,
|
bool localAudio,
|
||||||
int gamepadMask)
|
int gamepadMask)
|
||||||
{
|
{
|
||||||
|
int riKeyId;
|
||||||
|
|
||||||
|
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||||
|
riKeyId = qFromBigEndian(riKeyId);
|
||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"launch",
|
"launch",
|
||||||
@@ -132,7 +147,7 @@ NvHTTP::launchApp(int appId,
|
|||||||
QString::number(streamConfig->fps)+
|
QString::number(streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
(streamConfig->enableHdr ?
|
(streamConfig->enableHdr ?
|
||||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||||
"")+
|
"")+
|
||||||
@@ -149,11 +164,16 @@ NvHTTP::launchApp(int appId,
|
|||||||
void
|
void
|
||||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||||
{
|
{
|
||||||
|
int riKeyId;
|
||||||
|
|
||||||
|
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||||
|
riKeyId = qFromBigEndian(riKeyId);
|
||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"resume",
|
"resume",
|
||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||||
"&rikeyid="+QString::number(qFromBigEndian(*(int*)streamConfig->remoteInputAesIv))+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||||
false);
|
false);
|
||||||
|
|
||||||
@@ -175,7 +195,7 @@ NvHTTP::quitApp()
|
|||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requestor.
|
||||||
if (getCurrentGame(getServerInfo()) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
throw GfeHttpResponseException(599, "");
|
throw GfeHttpResponseException(599, "");
|
||||||
@@ -215,7 +235,8 @@ NvHTTP::getAppList()
|
|||||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true);
|
true,
|
||||||
|
NvLogLevel::ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -265,7 +286,10 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
QString statusMessage = xmlReader.attributes().value("status_message").toString();
|
QString statusMessage = xmlReader.attributes().value("status_message").toString();
|
||||||
qDebug() << "Request failed: " << statusCode << " " << statusMessage;
|
if (statusCode != 401) {
|
||||||
|
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
||||||
|
qWarning() << "Request failed:" << statusCode << statusMessage;
|
||||||
|
}
|
||||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +303,8 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true);
|
true,
|
||||||
|
NvLogLevel::VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -325,12 +350,15 @@ QString
|
|||||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout)
|
bool enableTimeout,
|
||||||
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
ret = QTextStream(reply).readAll();
|
QTextStream stream(reply);
|
||||||
|
stream.setCodec("UTF-8");
|
||||||
|
ret = stream.readAll();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -340,7 +368,8 @@ QNetworkReply*
|
|||||||
NvHTTP::openConnection(QUrl baseUrl,
|
NvHTTP::openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout)
|
bool enableTimeout,
|
||||||
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
@@ -366,13 +395,17 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
{
|
{
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
qDebug() << "Executing request: " << url.toString();
|
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||||
|
qInfo() << "Executing request:" << url.toString();
|
||||||
|
}
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
qDebug() << "Aborting timed out request for " << url.toString();
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +416,9 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
qDebug() << command << " request failed with error " << reply->error();
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
|
}
|
||||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||||
delete reply;
|
delete reply;
|
||||||
throw exception;
|
throw exception;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public:
|
|||||||
|
|
||||||
bool isInitialized()
|
bool isInitialized()
|
||||||
{
|
{
|
||||||
return id != 0 && !name.isNull();
|
return id != 0 && !name.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
int id;
|
int id;
|
||||||
@@ -80,6 +80,12 @@ private:
|
|||||||
class NvHTTP
|
class NvHTTP
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum NvLogLevel {
|
||||||
|
NONE,
|
||||||
|
ERROR,
|
||||||
|
VERBOSE
|
||||||
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address);
|
explicit NvHTTP(QString address);
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -87,7 +93,7 @@ public:
|
|||||||
getCurrentGame(QString serverInfo);
|
getCurrentGame(QString serverInfo);
|
||||||
|
|
||||||
QString
|
QString
|
||||||
getServerInfo();
|
getServerInfo(NvLogLevel logLevel);
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
@@ -107,7 +113,8 @@ public:
|
|||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout);
|
bool enableTimeout,
|
||||||
|
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -143,7 +150,8 @@ private:
|
|||||||
openConnection(QUrl baseUrl,
|
openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout);
|
bool enableTimeout,
|
||||||
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
|
|
||||||
#include <openssl/bio.h>
|
#include <openssl/bio.h>
|
||||||
#include <openssl/aes.h>
|
#include <openssl/aes.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
@@ -21,7 +19,7 @@ NvPairingManager::NvPairingManager(QString address) :
|
|||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
if (m_Cert == nullptr)
|
if (m_Cert == nullptr)
|
||||||
{
|
{
|
||||||
throw new std::runtime_error("Unable to load certificate");
|
throw std::runtime_error("Unable to load certificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
||||||
@@ -32,7 +30,7 @@ NvPairingManager::NvPairingManager(QString address) :
|
|||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
if (m_PrivateKey == nullptr)
|
if (m_PrivateKey == nullptr)
|
||||||
{
|
{
|
||||||
throw new std::runtime_error("Unable to load private key");
|
throw std::runtime_error("Unable to load private key");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,13 +87,15 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
|||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
BIO_free_all(bio);
|
BIO_free_all(bio);
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||||
ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||||
#else
|
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
const ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature;
|
||||||
#endif
|
|
||||||
|
|
||||||
X509_get0_signature(&asnSignature, NULL, cert);
|
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(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ NvPairingManager::PairState
|
|||||||
NvPairingManager::pair(QString appVersion, QString pin)
|
NvPairingManager::pair(QString appVersion, QString pin)
|
||||||
{
|
{
|
||||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||||
qDebug() << "Pairing with server generation: " << serverMajorVersion;
|
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||||
|
|
||||||
QCryptographicHash::Algorithm hashAlgo;
|
QCryptographicHash::Algorithm hashAlgo;
|
||||||
int hashLength;
|
int hashLength;
|
||||||
@@ -196,14 +196,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(getCert);
|
NvHTTP::verifyResponseStatus(getCert);
|
||||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #1";
|
qCritical() << "Failed pairing at stage #1";
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||||
if (serverCert == nullptr)
|
if (serverCert == nullptr)
|
||||||
{
|
{
|
||||||
qDebug() << "Server likely already pairing";
|
qCritical() << "Server likely already pairing";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(challengeXml);
|
NvHTTP::verifyResponseStatus(challengeXml);
|
||||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #2";
|
qCritical() << "Failed pairing at stage #2";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -228,13 +228,15 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
QByteArray challengeResponse;
|
QByteArray challengeResponse;
|
||||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||||
|
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
|
||||||
|
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
ASN1_BIT_STRING *asnSignature;
|
ASN1_BIT_STRING *asnSignature;
|
||||||
#else
|
|
||||||
const ASN1_BIT_STRING *asnSignature;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
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(challengeResponseData.data() + hashLength, 16);
|
||||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||||
@@ -249,7 +251,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(respXml);
|
NvHTTP::verifyResponseStatus(respXml);
|
||||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #3";
|
qCritical() << "Failed pairing at stage #3";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -262,7 +264,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
serverSignature,
|
serverSignature,
|
||||||
serverCert))
|
serverCert))
|
||||||
{
|
{
|
||||||
qDebug() << "MITM detected";
|
qCritical() << "MITM detected";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -273,7 +275,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
expectedResponseData.append(serverSecret);
|
expectedResponseData.append(serverSecret);
|
||||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||||
{
|
{
|
||||||
qDebug() << "Incorrect PIN";
|
qCritical() << "Incorrect PIN";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::PIN_WRONG;
|
return PairState::PIN_WRONG;
|
||||||
}
|
}
|
||||||
@@ -290,7 +292,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #4";
|
qCritical() << "Failed pairing at stage #4";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
@@ -302,7 +304,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qDebug() << "Failed pairing at stage #5";
|
qCritical() << "Failed pairing at stage #5";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop">
|
||||||
|
<id>com.moonlight_stream.Moonlight</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0+</project_license>
|
||||||
|
<name>Moonlight</name>
|
||||||
|
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||||
|
<p>Features include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Streaming at up to 4K resolution</li>
|
||||||
|
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
||||||
|
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||||
|
<li>5.1 surround sound audio support</li>
|
||||||
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
|
<li>Gamepad support with SDL mapping compatibility</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<url type="homepage">https://moonlight-stream.com</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>
|
||||||
|
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
|
||||||
|
<releases>
|
||||||
|
<release version="0.5.0" date="2018-09-15">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to enable mouse acceleration for remote desktop usage</li>
|
||||||
|
<li>Added option to disable automatic PC discovery</li>
|
||||||
|
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
|
||||||
|
<li>Added help links to error dialogs</li>
|
||||||
|
<li>Added quit shortcut tip to stream loading page</li>
|
||||||
|
<li>Restored the autodetect audio configuration option</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed quit app dialog starting the wrong app</li>
|
||||||
|
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
|
||||||
|
<li>Fixed sending errant mouse click when capturing the mouse</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.0" date="2018-09-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
|
||||||
|
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
|
||||||
|
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
|
||||||
|
<li>The streaming window is now centered on the target monitor</li>
|
||||||
|
<li>Display a dialog when the connection is terminated by the remote PC</li>
|
||||||
|
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming with no audio device</li>
|
||||||
|
<li>Fixed mouse lag with some high polling rate mice</li>
|
||||||
|
<li>Improved UI performance on the app grid page</li>
|
||||||
|
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
|
||||||
|
<li>Autodetect audio configuration option removed because it was broken</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.3.0" date="2018-08-21">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Reduced power consumption when the app is minimized</li>
|
||||||
|
<li>Fixed malformed asset requests being sent when the first host in the grid is offline</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.2.0" date="2018-08-12">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed functionality of '=', 'Numpad Dot', and 'Numpad Enter' keys</li>
|
||||||
|
<li>Fixed uninitialized variable causing strange gamepad behavior</li>
|
||||||
|
<li>Added a warning for incompatible configs and decoder failures</li>
|
||||||
|
<li>Added a warning for Wayland performance issues</li>
|
||||||
|
<li>Stopped capturing the mouse by default for windowed streams</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.1.0" date="2018-08-05">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Allow streaming at the client's native resolution</li>
|
||||||
|
<li>VDPAU hardware decoding support for NVIDIA GPUs</li>
|
||||||
|
<li>Updated UI to replace status text with icons</li>
|
||||||
|
<li>Stream window is now resizable</li>
|
||||||
|
<li>Display warnings during game launch for configuration issues</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Gamepad input now works even if the app doesn't have focus</li>
|
||||||
|
<li>Fixed streaming black screen if the host supported HEVC but the client didn't</li>
|
||||||
|
<li>Fixed broken video scaling causing extra stretching and off-center video</li>
|
||||||
|
<li>Fixed gamepad input issue causing some Y axis inputs to be reversed</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.0.5" date="2018-08-01">
|
||||||
|
<description>
|
||||||
|
<p>Changes from 0.0.4:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed a bug causing increased frame drops and input lag</li>
|
||||||
|
<li>Added support for quitting running games</li>
|
||||||
|
<li>Added support for waking sleeping PCs with Wake-on-LAN</li>
|
||||||
|
<li>Increased the FPS limit to 120 FPS</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.0.4a" date="2018-07-30">
|
||||||
|
<description>
|
||||||
|
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>Game</category>
|
||||||
|
<category>Qt</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||||
|
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>moonlight</binary>
|
||||||
|
</provides>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.0" />
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Moonlight
|
||||||
|
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
||||||
|
Exec=moonlight
|
||||||
|
Icon=moonlight
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Qt;Game;
|
||||||
|
Keywords=nvidia;gamestream;stream;
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# Game Controller DB for SDL in 2.0.6 format
|
||||||
|
# Source: https://github.com/gabomdq/SDL_GameControllerDB
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
03000000fa2d00000100000000000000,3DRUDDER,leftx:a0,lefty:a1,rightx:a5,righty:a2,platform:Windows,
|
||||||
|
03000000022000000090000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
|
||||||
|
03000000203800000900000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
|
||||||
|
03000000c82d00000060000000000000,8Bitdo SF30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
|
||||||
|
03000000c82d00000061000000000000,8Bitdo SF30 Pro Wireless,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,platform:Windows,
|
||||||
|
03000000102800000900000000000000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Windows,
|
||||||
|
03000000a00500003232000000000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Windows,
|
||||||
|
030000008f0e00001200000000000000,Acme,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000341a00003608000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000c01100001352000000000000,Battalife Joystick,a:b6,b:b7,back:b2,leftshoulder:b0,leftx:a0,lefty:a1,rightshoulder:b1,start:b3,x:b4,y:b5,platform:Windows,
|
||||||
|
030000006b1400000055000000000000,bigben ps3padstreetnew,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
0300000066f700000500000000000000,BrutalLegendTest,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000d81d00000b00000000000000,BUFFALO BSGP1601 Series ,a:b5,b:b3,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b8,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b9,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b13,x:b4,y:b2,platform:Windows,
|
||||||
|
03000000e82000006058000000000000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
030000005e0400008e02000000000000,Controller (XBOX 360 For Windows),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000260900008888000000000000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a4,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000a306000022f6000000000000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000791d00000103000000000000,Dual Box WII,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
030000004f04000023b3000000000000,Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000341a00000108000000000000,EXEQ RF USB Gamepad 8206,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008500000000000000,Fighting Commander 2016 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008400000000000000,Fighting Commander 5,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008700000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008800000000000000,Fighting Stick mini 4,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00002700000000000000,FIGHTING STICK V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
78696e70757403000000000000000000,Fightstick TES,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000790000000600000000000000,G-Shark GS-GP702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000260900002625000000000000,Gamecube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,lefttrigger:a4,leftx:a0,lefty:a1,righttrigger:a5,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
030000008f0e00000d31000000000000,GAMEPAD 3 TURBO,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000280400000140000000000000,GamePad Pro USB,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000ffff00000000000000000000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000451300000010000000000000,Generic USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000341a00000302000000000000,Hama Scorpad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00004900000000000000,Hatsune Miku Sho Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000d81400000862000000000000,HitBox Edition Cthulhu+,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00005f00000000000000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00005e00000000000000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00004000000000000000,Hori Fighting Stick Mini 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b4,rightshoulder:b7,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00006e00000000000000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00006600000000000000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f0000ee00000000000000,HORIPAD mini4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00004d00000000000000,HORIPAD3 A,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000250900000017000000000000,HRAP2 on PS/SS/N64 Joypad to USB BOX,a:b2,b:b1,back:b9,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b8,x:b3,y:b0,platform:Windows,
|
||||||
|
030000008f0e00001330000000000000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000d81d00000f00000000000000,iBUFFALO BSGP1204 Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000d81d00001000000000000000,iBUFFALO BSGP1204P Series,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000830500006020000000000000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Windows,
|
||||||
|
03000000b50700001403000000000000,IMPACT BLACK,a:b2,b:b3,back:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
|
||||||
|
030000006f0e00002401000000000000,INJUSTICE FightStick for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000491900000204000000000000,Ipega PG-9023,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b3,y:b4,platform:Windows,
|
||||||
|
030000006d04000011c2000000000000,Logitech Cordless Wingman,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b5,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b2,righttrigger:b7,rightx:a3,righty:a4,x:b4,platform:Windows,
|
||||||
|
030000006d04000016c2000000000000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000006d04000018c2000000000000,Logitech F510 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000006d04000019c2000000000000,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700005032000000000000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700005082000000000000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008433000000000000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008483000000000000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b6,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008134000000000000,Mad Catz FightStick TE2+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b7,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b4,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008184000000000000,Mad Catz FightStick TE2+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,leftstick:b10,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b4,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008034000000000000,Mad Catz TE2 PS3 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008084000000000000,Mad Catz TE2 PS4 Fightstick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700008532000000000000,Madcatz Arcade Fightstick TE S PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700003888000000000000,Madcatz Arcade Fightstick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000380700001888000000000000,MadCatz SFIV FightStick PS3,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b5,lefttrigger:b7,leftx:a0,lefty:a1,rightshoulder:b4,righttrigger:b6,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000380700008081000000000000,MADCATZ SFV Arcade FightStick Alpha PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000008305000031b0000000000000,MaxfireBlaze3,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000250900000128000000000000,Mayflash Arcade Stick,a:b1,b:b2,back:b8,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b5,y:b6,platform:Windows,
|
||||||
|
03000000790000004418000000000000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000790000004318000000000000,Mayflash GameCube Controller Adapter,a:b1,b:b2,back:b0,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b0,leftshoulder:b4,leftstick:b0,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b0,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000008f0e00001030000000000000,Mayflash USB Adapter for original Sega Saturn controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b5,rightshoulder:b2,righttrigger:b7,start:b9,x:b3,y:b4,platform:Windows,
|
||||||
|
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Windows,
|
||||||
|
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000250900006688000000000000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
|
||||||
|
030000001008000001e5000000000000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000bd12000015d0000000000000,Nintendo Retrolink USB Super SNES Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
030000004b120000014d000000000000,NYKO AIRFLO,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:a3,leftstick:a0,lefttrigger:b6,rightshoulder:b5,rightstick:a2,righttrigger:b7,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000362800000100000000000000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,x:b1,y:b2,platform:Windows,
|
||||||
|
03000000120c0000f60e000000000000,P4 Wired Gamepad,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b7,rightshoulder:b4,righttrigger:b6,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000d62000006dca000000000000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000008f0e00007530000000000000,PS (R) Gamepad,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b1,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000e30500009605000000000000,PS to USB convert cable,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000100800000100000000000000,PS1 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000100800000300000000000000,PS2 USB,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a4,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Windows,
|
||||||
|
03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000250900000500000000000000,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000100000008200000000000000,PS360+ v1.66,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:h0.4,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000004c050000a00b000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000004c050000cc09000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00000011000000000000,QanBa Arcade JoyStick 1008,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b10,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00001611000000000000,QanBa Arcade JoyStick 4018,a:b1,b:b2,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b8,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000222c00000020000000000000,QANBA DRONE ARCADE JOYSTICK,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,rightshoulder:b5,righttrigger:a4,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00001210000000000000,QanBa Joystick Plus,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000341a00000104000000000000,QanBa Joystick Q4RAF,a:b5,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b0,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b3,righttrigger:b7,start:b9,x:b1,y:b2,platform:Windows,
|
||||||
|
03000000222c00000223000000000000,Qanba Obsidian Arcade Joystick PS3 Mode,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000222c00000023000000000000,Qanba Obsidian Arcade Joystick PS4 Mode,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000321500000003000000000000,Razer Hydra,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a2,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
030000000d0f00001100000000000000,REAL ARCADE PRO.3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00006a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00006b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008a00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00008b00000000000000,Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00007000000000000000,REAL ARCADE PRO.4 VLX,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,rightshoulder:b5,rightstick:b11,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00002200000000000000,REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00005b00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000000d0f00005c00000000000000,Real Arcade Pro.V4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000790000001100000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
0300000000f000000300000000000000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
|
||||||
|
0300000000f00000f100000000000000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Windows,
|
||||||
|
030000006b140000010d000000000000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000006f0e00001e01000000000000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000004f04000003d0000000000000,run'n'drive,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b7,leftshoulder:a3,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:a4,rightstick:b11,righttrigger:b5,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000a30600001af5000000000000,Saitek Cyborg,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000a306000023f6000000000000,Saitek Cyborg V.1 Game pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00001201000000000000,Saitek Dual Analog Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
|
||||||
|
03000000a30600000cff000000000000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,x:b0,y:b1,platform:Windows,
|
||||||
|
03000000a30600000c04000000000000,Saitek P2900,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00001001000000000000,Saitek P480 Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
|
||||||
|
03000000a30600000b04000000000000,Saitek P990,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000300f00001101000000000000,saitek rumble pad,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
|
||||||
|
0300000000050000289b000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
030000009b2800000500000000000000,Saturn_Adapter_2.0,a:b1,b:b2,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000341a00000208000000000000,SL-6555-SBK,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:-a4,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a3,righty:a2,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
030000008f0e00000800000000000000,SpeedLink Strike FX Wireless,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000ff1100003133000000000000,SVEN X-PAD,a:b2,b:b3,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b9,rightx:a2,righty:a4,start:b5,x:b0,y:b1,platform:Windows,
|
||||||
|
03000000fa1900000706000000000000,Team 5,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000b50700001203000000000000,Techmobility X6-38V,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Windows,
|
||||||
|
030000004f04000015b3000000000000,Thrustmaster Dual Analog 2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
|
||||||
|
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Windows,
|
||||||
|
030000004f04000004b3000000000000,Thrustmaster Firestorm Dual Power 3,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Windows,
|
||||||
|
03000000666600000488000000000000,TigerGame PS/PS2 Game Controller Adapter,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000d90400000200000000000000,TwinShock PS2,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000380700006652000000000000,UnKnown,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000632500002305000000000000,USB Vibration Joystick (BM),a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Windows,
|
||||||
|
03000000790000001b18000000000000,Venom Arcade Joystick,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Windows,
|
||||||
|
03000000450c00002043000000000000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Windows,
|
||||||
|
03000000172700004431000000000000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a7,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Windows,
|
||||||
|
03000000786901006e70000000000000,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Windows,
|
||||||
|
|
||||||
|
# Mac OS X
|
||||||
|
03000000022000000090000001000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
|
||||||
|
03000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Mac OS X,
|
||||||
|
03000000102800000900000000000000,8Bitdo SFC30 GamePad Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
|
||||||
|
03000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
|
||||||
|
03000000a00500003232000009010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Mac OS X,
|
||||||
|
030000008305000031b0000000000000,Cideko AK08b,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000260900008888000088020000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000a306000022f6000001030000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000790000000600000000000000,G-Shark GP-702,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
03000000ad1b000001f9000000000000,Gamestop BB-070 X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00005f00000000010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00005e00000000010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00005f00000000000000,HORI Fighting Commander 4 PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00005e00000000000000,HORI Fighting Commander 4 PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00004d00000000000000,HORI Gem Pad 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00006e00000000010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00006600000000010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000000d0f00006600000000000000,HORIPAD FPS PLUS 4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000008f0e00001330000011010000,HuiJia SNES Controller,a:b4,b:b2,back:b16,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b12,rightshoulder:b14,start:b18,x:b6,y:b0,platform:Mac OS X,
|
||||||
|
03000000830500006020000000010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
|
||||||
|
03000000830500006020000000000000,iBuffalo USB 2-axis 8-button Gamepad,a:b1,b:b0,back:b6,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Mac OS X,
|
||||||
|
030000007e0500000620000001000000,Joy-Con (L),+leftx:h0.2,+lefty:h0.4,-leftx:h0.8,-lefty:h0.1,a:b0,b:b1,leftshoulder:b4,leftstick:b10,rightshoulder:b5,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000016c2000000020000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000016c2000000030000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000016c2000014040000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000016c2000000000000,Logitech F310 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000018c2000000000000,Logitech F510 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000006d0400001fc2000000000000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000006d04000019c2000000000000,Logitech Wireless Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000380700005032000000010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000380700005082000000010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000790000004418000000010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
0300000025090000e803000000000000,Mayflash Wii Classic Controller,a:b1,b:b0,back:b8,dpdown:b13,dpleft:b12,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Mac OS X,
|
||||||
|
03000000790000000018000000000000,Mayflash WiiU Pro Game Controller Adapter (DInput),a:b4,b:b8,back:b32,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b16,leftstick:b40,lefttrigger:b24,leftx:a0,lefty:a4,rightshoulder:b20,rightstick:b44,righttrigger:b28,rightx:a8,righty:a12,start:b36,x:b0,y:b12,platform:Mac OS X,
|
||||||
|
03000000d8140000cecf000000000000,MC Cthulhu,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000001008000001e5000006010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000008f0e00000300000000000000,Piranha xtreme,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
03000000d62000006dca000000010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
|
||||||
|
030000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Mac OS X,
|
||||||
|
030000004c050000a00b000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000008916000000fd000000000000,Razer Onza TE,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000321500000010000000010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
0300000032150000030a000000000000,Razer Wildcat,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000790000001100000000000000,Retrolink Classic Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a3,lefty:a4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
03000000790000001100000006010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
030000006b140000010d000000010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
03000000c6240000fefa000000000000,Rock Candy Gamepad for PS3,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000811700007e05000000000000,Sega Saturn,a:b2,b:b4,dpdown:b16,dpleft:b15,dpright:b14,dpup:b17,leftshoulder:b8,lefttrigger:a5,leftx:a0,lefty:a2,rightshoulder:b9,righttrigger:a4,start:b13,x:b0,y:b6,platform:Mac OS X,
|
||||||
|
03000000b40400000a01000000000000,Sega Saturn USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Mac OS X,
|
||||||
|
030000003512000021ab000000000000,SFC30 Joystick,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Mac OS X,
|
||||||
|
030000004c050000cc09000000000000,Sony DualShock 4 V2,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000004c050000a00b000000000000,Sony DualShock 4 Wireless Adaptor,a:b1,b:b2,back:b13,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
030000005e0400008e02000001000000,Steam Virtual GamePad,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000110100002014000000000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b12,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000110100002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000381000002014000001000000,SteelSeries Nimbus,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000110100001714000000000000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000110100001714000020010000,SteelSeries Stratus XL,a:b0,b:b1,dpdown:b9,dpleft:b11,dpright:b10,dpup:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1~,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3~,start:b12,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000004f04000015b3000000000000,Thrustmaster Dual Analog 3.2,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Mac OS X,
|
||||||
|
030000004f04000000b3000000000000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Mac OS X,
|
||||||
|
03000000bd12000015d0000000000000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,leftshoulder:b4,leftx:a0,lefty:a1,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
03000000bd12000015d0000000010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Mac OS X,
|
||||||
|
03000000100800000100000000000000,Twin USB Joystick,a:b4,b:b2,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b12,leftstick:b20,lefttrigger:b8,leftx:a0,lefty:a2,rightshoulder:b14,rightstick:b22,righttrigger:b10,rightx:a6,righty:a4,start:b18,x:b6,y:b0,platform:Mac OS X,
|
||||||
|
050000005769696d6f74652028303000,Wii Remote,a:b4,b:b5,back:b7,dpdown:b3,dpleft:b0,dpright:b1,dpup:b2,guide:b8,leftshoulder:b11,lefttrigger:b12,leftx:a0,lefty:a1,start:b6,x:b10,y:b9,platform:Mac OS X,
|
||||||
|
050000005769696d6f74652028313800,Wii U Pro Controller,a:b16,b:b15,back:b7,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b8,leftshoulder:b19,leftstick:b23,lefttrigger:b21,leftx:a0,lefty:a1,rightshoulder:b20,rightstick:b24,righttrigger:b22,rightx:a2,righty:a3,start:b6,x:b18,y:b17,platform:Mac OS X,
|
||||||
|
030000005e0400008e02000000000000,X360 Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
03000000c6240000045d000000000000,Xbox 360 Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000d102000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000dd02000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000e302000000000000,Xbox One Wired Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000e002000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000e002000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000ea02000000000000,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b10,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b8,x:b2,y:b3,platform:Mac OS X,
|
||||||
|
030000005e040000fd02000003090000,Xbox Wireless Controller,a:b0,b:b1,back:b16,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Mac OS X,
|
||||||
|
03000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b15,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Mac OS X,
|
||||||
|
03000000120c0000100e000000010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Mac OS X,
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
03000000022000000090000011010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
05000000203800000900000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
05000000c82d00002038000000010000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b2,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
03000000c82d00000190000011010000,8Bitdo NES30 Pro 8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a5,rightx:a2,righty:a3,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
05000000102800000900000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
05000000c82d00003028000000010000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,platform:Linux,
|
||||||
|
05000000a00500003232000001000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
|
||||||
|
05000000a00500003232000008010000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,platform:Linux,
|
||||||
|
030000006f0e00003901000000430000,Afterglow Prismatic Wired Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006f0e00003901000020060000,Afterglow Wired Controller for Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000100000008200000011010000,Akishop Customs PS360+ v1.66,a:b1,b:b2,back:b12,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
05000000050b00000045000031000000,ASUS Gamepad,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b6,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b10,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000666600006706000000010000,boom PSX to PC Converter,a:b2,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a2,righty:a3,start:b11,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000e82000006058000001010000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000260900008888000000010000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a5,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000a306000022f6000011010000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000b40400000a01000000010000,CYPRESS USB Gamepad,a:b0,b:b1,back:b5,guide:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
|
||||||
|
03000000790000000600000010010000,DragonRise Inc. Generic USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
030000006f0e00003001000001010000,EA Sports PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000341a000005f7000010010000,GameCube {HuiJia USB box},a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
0500000047532047616d657061640000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006f0e00000104000000010000,Gamestop Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006f0e00001304000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:a0,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:a3,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006f0e00001f01000000010000,Generic X-Box pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000f0250000c183000010010000,Goodbetterbest Ltd USB Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
0300000079000000d418000000010000,GPD Win 2 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000280400000140000000010000,Gravis GamePad Pro USB ,a:b1,b:b2,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000008f0e00000610000000010000,GreenAsia Electronics 4Axes 12Keys GamePad ,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b9,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b10,righttrigger:b5,rightx:a3,righty:a2,start:b11,x:b3,y:b0,platform:Linux,
|
||||||
|
030000008f0e00000300000010010000,GreenAsia Inc. USB Joystick,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
030000008f0e00001200000010010000,GreenAsia Inc. USB Joystick,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
0500000047532067616d657061640000,GS gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
06000000adde0000efbe000002010000,Hidromancer Game Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000d81400000862000011010000,HitBox (PS3/PC) Analog Mode,a:b1,b:b2,back:b8,guide:b9,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b12,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000c9110000f055000011010000,HJC Game GAMEPAD,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
030000000d0f00000d00000000010000,hori,a:b0,b:b6,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftx:b4,lefty:b5,rightshoulder:b7,start:b9,x:b1,y:b2,platform:Linux,
|
||||||
|
030000000d0f00001000000011010000,HORI CO. LTD. FIGHTING STICK 3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00006a00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00006b00000011010000,HORI CO. LTD. Real Arcade Pro.4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00002200000011010000,HORI CO. LTD. REAL ARCADE Pro.V3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,rightshoulder:b5,righttrigger:b7,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00005f00000011010000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00005e00000011010000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000ad1b000001f5000033050000,Hori Pad EX Turbo 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000000d0f00006e00000011010000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00006600000011010000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000000d0f00006700000001010000,HORIPAD ONE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000008f0e00001330000010010000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000830500006020000010010000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,platform:Linux,
|
||||||
|
050000006964726f69643a636f6e0000,idroid:con,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000b50700001503000010010000,impact,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
|
||||||
|
03000000fd0500000030000000010000,InterAct GoPad I-73000 (Fighting Game Layout),a:b3,b:b4,back:b6,leftx:a0,lefty:a1,rightshoulder:b2,righttrigger:b5,start:b7,x:b0,y:b1,platform:Linux,
|
||||||
|
0500000049190000020400001b010000,Ipega PG-9069 - Bluetooth Gamepad,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b161,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
|
||||||
|
030000006e0500000320000010010000,JC-U3613M - DirectInput Mode,a:b2,b:b3,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,start:b11,x:b0,y:b1,platform:Linux,
|
||||||
|
03000000300f00001001000010010000,Jess Tech Dual Analog Rumble Pad,a:b2,b:b3,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b0,y:b1,platform:Linux,
|
||||||
|
03000000ba2200002010000001010000,Jess Technology USB Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
030000006f0e00000103000000020000,Logic3 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006d04000019c2000010010000,Logitech Cordless RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006d04000016c2000010010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006d04000016c2000011010000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006d0400001dc2000014400000,Logitech F310 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006d0400001ec2000020200000,Logitech F510 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006d04000019c2000011010000,Logitech F710 Gamepad (DInput),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006d0400001fc2000005030000,Logitech F710 Gamepad (XInput),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000006d04000015c2000010010000,Logitech Logitech Extreme 3D,a:b0,b:b4,back:b6,guide:b8,leftshoulder:b9,leftstick:h0.8,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:h0.2,start:b7,x:b2,y:b5,platform:Linux,
|
||||||
|
030000006d04000018c2000010010000,Logitech RumblePad 2,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006d04000011c2000010010000,Logitech WingMan Cordless RumblePad,a:b0,b:b1,back:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b6,lefttrigger:b9,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b10,rightx:a3,righty:a4,start:b8,x:b3,y:b4,platform:Linux,
|
||||||
|
05000000380700006652000025010000,Mad Catz C.T.R.L.R ,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700005032000011010000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700005082000011010000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000ad1b00002ef0000090040000,Mad Catz Fightpad SFxT,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,rightshoulder:b5,righttrigger:a5,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000380700008034000011010000,Mad Catz fightstick (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700008084000011010000,Mad Catz fightstick (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700008433000011010000,Mad Catz FightStick TE S+ PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700008483000011010000,Mad Catz FightStick TE S+ PS4,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700001647000010040000,Mad Catz Wired Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000380700003847000090040000,Mad Catz Wired Xbox 360 Controller (SFIV),a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000ad1b000016f0000090040000,Mad Catz Xbox 360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000380700001888000010010000,MadCatz PC USB Wired Stick 8818,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000380700003888000010010000,MadCatz PC USB Wired Stick 8838,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:a0,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
0300000079000000d218000011010000,MAGIC-NS,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000790000004418000010010000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000780000000600000010010000,Microntek USB Joystick,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
030000005e0400000e00000000010000,Microsoft SideWinder,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,rightshoulder:b7,start:b8,x:b3,y:b4,platform:Linux,
|
||||||
|
030000005e0400008e02000004010000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400008e02000062230000,Microsoft X-Box 360 pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000e302000003020000,Microsoft X-Box One Elite pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000d102000001010000,Microsoft X-Box One pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000d102000003020000,Microsoft X-Box One pad v2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400008502000000010000,Microsoft X-Box pad (Japan),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
|
||||||
|
030000005e0400008902000021010000,Microsoft X-Box pad v2 (US),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
|
||||||
|
05000000d6200000ad0d000001000000,Moga Pro,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b7,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b8,righttrigger:a4,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000250900006688000000010000,MP-8866 Super Dual Box,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
|
||||||
|
030000001008000001e5000010010000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
050000007e0500000920000001000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
050000007e0500003003000001000000,Nintendo Wii Remote Pro Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
05000000010000000100000003000000,Nintendo Wiimote,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
030000000d0500000308000010010000,Nostromo n45 Dual Analog Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b4,leftstick:b12,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b10,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000550900001072000011010000,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b13,leftshoulder:b4,leftstick:b8,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000451300000830000010010000,NYKO CORE,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000005e0400000202000000010000,Old Xbox pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b5,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b2,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b3,y:b4,platform:Linux,
|
||||||
|
05000000362800000100000002010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
|
||||||
|
05000000362800000100000003010000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,x:b1,y:b2,platform:Linux,
|
||||||
|
03000000ff1100003133000010010000,PC Game Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
030000006f0e00006401000001010000,PDP Battlefield One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000d62000006dca000011010000,PowerA Pro Ex,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000ff1100004133000010010000,PS2 Controller,a:b2,b:b1,back:b8,leftshoulder:b6,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000341a00003608000011010000,PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004c0500006802000010010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
|
||||||
|
030000004c0500006802000010810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
030000004c0500006802000011010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
|
||||||
|
030000004c0500006802000011810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
050000004c0500006802000000010000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:a12,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:a13,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
|
||||||
|
050000004c0500006802000000810000,PS3 Controller,a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
050000004c0500006802000000800000,PS3 Controller (Bluetooth),a:b0,b:b1,back:b8,dpdown:b14,dpleft:b15,dpright:b16,dpup:b13,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
05000000504c415953544154494f4e00,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
|
||||||
|
060000004c0500006802000000010000,PS3 Controller (Bluetooth),a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,platform:Linux,
|
||||||
|
030000004c050000a00b000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004c050000a00b000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
030000004c050000c405000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004c050000c405000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
030000004c050000cc09000011010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004c050000cc09000011810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
050000004c050000c405000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
050000004c050000c405000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
050000004c050000cc09000000010000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
050000004c050000cc09000000810000,PS4 Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b11,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b12,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
03000000300f00001211000011010000,QanBa Arcade JoyStick,a:b2,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b5,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:b6,start:b9,x:b1,y:b3,platform:Linux,
|
||||||
|
030000009b2800000300000001010000,raphnet.net 4nes4snes v1.5,a:b0,b:b4,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b1,y:b5,platform:Linux,
|
||||||
|
030000008916000001fd000024010000,Razer Onza Classic Edition,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000008916000000fd000024010000,Razer Onza Tournament,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000321500000010000011010000,Razer RAIJU,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000c6240000045d000025010000,Razer Sabertooth,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000321500000009000011010000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
050000003215000000090000163a0000,Razer Serval,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
0300000032150000030a000001010000,Razer Wildcat,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000790000001100000010010000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
0300000000f000000300000000010000,RetroUSB.com RetroPad,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
|
||||||
|
0300000000f00000f100000000010000,RetroUSB.com Super RetroPort,a:b1,b:b5,back:b2,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b3,x:b0,y:b4,platform:Linux,
|
||||||
|
030000006b140000010d000011010000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006f0e00001e01000011010000,Rock Candy Gamepad for PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000006f0e00004601000001010000,Rock Candy Wired Controller for Xbox One,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000a306000023f6000011010000,Saitek Cyborg V.1 Game Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000a30600000cff000010010000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
|
||||||
|
03000000a30600000c04000011010000,Saitek P2900 Wireless Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b9,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b12,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000a30600000901000000010000,Saitek P880,a:b2,b:b3,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a3,righty:a2,x:b0,y:b1,platform:Linux,
|
||||||
|
03000000a30600000b04000000010000,Saitek P990 Dual Analog Pad,a:b1,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a2,start:b8,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000a306000018f5000010010000,Saitek PLC Saitek P3200 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a3,righty:a4,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000c01600008704000011010000,Serial/Keyboard/Mouse/Joystick,a:b12,b:b10,back:b4,dpdown:b2,dpleft:b3,dpright:b1,dpup:b0,leftshoulder:b9,leftstick:b14,lefttrigger:b6,leftx:a1,lefty:a0,rightshoulder:b8,rightstick:b15,righttrigger:b7,rightx:a2,righty:a3,start:b5,x:b13,y:b11,platform:Linux,
|
||||||
|
03000000f025000021c1000010010000,ShanWan Gioteck PS3 Wired Controller,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000632500007505000010010000,SHANWAN PS3/PC Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000250900000500000000010000,Sony PS2 pad with SmartJoy adapter,a:b2,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
|
||||||
|
030000005e0400008e02000073050000,Speedlink TORID Wireless Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400008e02000020200000,SpeedLink XEOX Pro Analog Gamepad pad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000de2800000112000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000de2800000211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000de2800004211000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000de280000fc11000001000000,Steam Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
05000000de2800000212000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000de280000ff11000001000000,Steam Virtual Gamepad,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000ad1b000038f0000090040000,Street Fighter IV FightStick TE,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000666600000488000000010000,Super Joy Box 5 Pro,a:b2,b:b1,back:b9,dpdown:b14,dpleft:b15,dpright:b13,dpup:b12,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b3,y:b0,platform:Linux,
|
||||||
|
030000004f04000020b3000010010000,Thrustmaster 2 in 1 DT,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
|
||||||
|
030000004f04000015b3000010010000,Thrustmaster Dual Analog 4,a:b0,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b1,y:b3,platform:Linux,
|
||||||
|
030000004f04000023b3000000010000,Thrustmaster Dual Trigger 3-in-1,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004f04000000b3000010010000,Thrustmaster Firestorm Dual Power,a:b0,b:b2,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b11,lefttrigger:b5,leftx:a0,lefty:a1,rightshoulder:b6,rightstick:b12,righttrigger:b7,rightx:a2,righty:a3,start:b10,x:b1,y:b3,platform:Linux,
|
||||||
|
030000004f04000008d0000000010000,Thrustmaster Run N Drive Wireless,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
030000004f04000009d0000000010000,Thrustmaster Run N Drive Wireless PS3,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
03000000bd12000015d0000010010000,Tomee SNES USB Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000d814000007cd000011010000,Toodles 2008 Chimp PC/PS3,a:b0,b:b1,back:b8,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,start:b9,x:b3,y:b2,platform:Linux,
|
||||||
|
03000000100800000100000010010000,Twin USB PS2 Adapter,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000100800000300000010010000,USB Gamepad,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b3,y:b0,platform:Linux,
|
||||||
|
03000000790000001100000000010000,USB Gamepad1,a:b2,b:b1,back:b8,dpdown:a0,dpleft:a1,dpright:a2,dpup:a4,start:b9,platform:Linux,
|
||||||
|
05000000ac0500003232000001000000,VR-BOX,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a3,righty:a2,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400008e02000010010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400008e02000014010000,X360 Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400001907000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e0400009102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000a102000000010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000a102000007010000,X360 Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
0000000058626f782033363020576900,Xbox 360 Wireless Controller,a:b0,b:b1,back:b14,dpdown:b11,dpleft:b12,dpright:b13,dpup:b10,guide:b7,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:Linux,
|
||||||
|
030000005e040000a102000014010000,Xbox 360 Wireless Receiver (XBOX),a:b0,b:b1,back:b6,dpdown:b14,dpleft:b11,dpright:b12,dpup:b13,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
0000000058626f782047616d65706100,Xbox Gamepad (userspace driver),a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a4,rightx:a2,righty:a3,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
050000005e040000e002000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
050000005e040000fd02000003090000,Xbox One Wireless Controller,a:b0,b:b1,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b16,leftshoulder:b6,leftstick:b13,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a4,rightx:a2,righty:a3,start:b11,x:b3,y:b4,platform:Linux,
|
||||||
|
03000000450c00002043000010010000,XEOX Gamepad SL-6556-BK,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,platform:Linux,
|
||||||
|
05000000172700004431000029010000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:a7,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a6,rightx:a2,righty:a5,start:b11,x:b3,y:b4,platform:Linux,
|
||||||
|
03000000c0160000e105000001010000,Xin-Mo Xin-Mo Dual Arcade,a:b4,b:b3,back:b6,dpdown:b12,dpleft:b13,dpright:b14,dpup:b11,guide:b9,leftshoulder:b2,leftx:a0,lefty:a1,rightshoulder:b5,start:b7,x:b1,y:b0,platform:Linux,
|
||||||
|
xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux,
|
||||||
|
03000000120c0000100e000011010000,ZEROPLUS P4 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,platform:Linux,
|
||||||
|
|
||||||
|
# Android
|
||||||
|
64633436313965656664373634323364,Microsoft X-Box 360 pad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,x:b2,y:b3,platform:Android,
|
||||||
|
37336435666338653565313731303834,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
|
||||||
|
4e564944494120436f72706f72617469,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
|
||||||
|
61363931656135336130663561616264,NVIDIA Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,platform:Android,
|
||||||
|
35643031303033326130316330353564,PS4 Controller,a:b1,b:b17,back:b15,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b3,leftstick:b4,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b6,righttrigger:+a4,rightx:a2,righty:a5,start:b16,x:b0,y:b2,platform:Android,
|
||||||
|
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:Android,
|
||||||
|
5477696e20555342204a6f7973746963,Twin USB Joystick,a:b22,b:b21,back:b28,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b26,leftstick:b30,lefttrigger:b24,leftx:a0,lefty:a1,rightshoulder:b27,rightstick:b31,righttrigger:b25,rightx:a3,righty:a2,start:b29,x:b23,y:b20,platform:Android,
|
||||||
|
34356136633366613530316338376136,Xbox Wireless Controller,a:b0,b:b1,back:b9,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b3,leftstick:b15,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b18,rightstick:b16,righttrigger:a5,rightx:a3,righty:a4,x:b17,y:b2,platform:Android,
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
4d466947616d65706164010000000000,MFi Extended Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a5,rightx:a3,righty:a4,start:b6,x:b2,y:b3,platform:iOS,
|
||||||
|
4d466947616d65706164020000000000,MFi Gamepad,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,rightshoulder:b5,start:b6,x:b2,y:b3,platform:iOS,
|
||||||
|
05000000de2800000511000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,platform:iOS,
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
@@ -12,29 +13,28 @@ GridView {
|
|||||||
id: appGrid
|
id: appGrid
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 5
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 225; cellHeight: 350;
|
cellWidth: 225; cellHeight: 385;
|
||||||
focus: true
|
|
||||||
|
|
||||||
// Cache delegates for 1000px in both directions to improve
|
|
||||||
// scrolling and resizing performance
|
|
||||||
cacheBuffer: 1000
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
// routine to run, but only if we start as invisible
|
// routine to run, but only if we start as invisible
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
|
function computerLost()
|
||||||
|
{
|
||||||
|
// Go back to the PC view on PC loss
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// Start polling when this view is shown
|
appModel.computerLost.connect(computerLost)
|
||||||
ComputerManager.startPolling()
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Stop polling when this view is not on top
|
appModel.computerLost.disconnect(computerLost)
|
||||||
ComputerManager.stopPollingAsync()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,18 +47,13 @@ GridView {
|
|||||||
|
|
||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ItemDelegate {
|
||||||
width: 200; height: 300;
|
width: 200; height: 335;
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (model.running) {
|
|
||||||
appNameText.text = "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + appNameText.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter;
|
||||||
|
y: 20
|
||||||
source: model.boxart
|
source: model.boxart
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 150
|
width: 150
|
||||||
@@ -66,31 +61,112 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: runningIcon
|
||||||
|
anchors.centerIn: appIcon
|
||||||
|
visible: model.running
|
||||||
|
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: appNameText
|
id: appNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
color: "white"
|
color: "white"
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 100
|
height: 125
|
||||||
anchors.top: appIcon.bottom
|
anchors.top: appIcon.bottom
|
||||||
font.pointSize: 26
|
font.pointSize: 22
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchOrResumeSelectedApp()
|
||||||
|
{
|
||||||
|
var runningIndex = appModel.getRunningAppIndex()
|
||||||
|
if (runningIndex >= 0 && runningIndex !== index) {
|
||||||
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
|
quitAppDialog.segueToStream = true
|
||||||
|
quitAppDialog.nextAppName = model.name
|
||||||
|
quitAppDialog.nextAppIndex = index
|
||||||
|
quitAppDialog.open()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
|
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
|
||||||
|
stackView.push(segue)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
// Nothing is running or this app is running
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// TODO: Check if a different game is running
|
// Right click
|
||||||
|
appContextMenu.open()
|
||||||
var component = Qt.createComponent("StreamSegue.qml")
|
|
||||||
var segue = component.createObject(stackView)
|
|
||||||
segue.appname = model.name
|
|
||||||
segue.session = appModel.createSessionForApp(index)
|
|
||||||
stackView.push(segue)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: appContextMenu
|
||||||
|
MenuItem {
|
||||||
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
|
onTriggered: {
|
||||||
|
appContextMenu.close()
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
|
height: visible ? implicitHeight : 0
|
||||||
|
}
|
||||||
|
MenuItem {
|
||||||
|
text: "Quit Game"
|
||||||
|
onTriggered: {
|
||||||
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
|
quitAppDialog.segueToStream = false
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
visible: model.running
|
||||||
|
height: visible ? implicitHeight : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: quitAppDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
property string appName : ""
|
||||||
|
property bool segueToStream : false
|
||||||
|
property string nextAppName: ""
|
||||||
|
property int nextAppIndex: 0
|
||||||
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: {
|
||||||
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
|
var params = {"appName": appName}
|
||||||
|
if (segueToStream) {
|
||||||
|
// Store the session and app name if we're going to stream after
|
||||||
|
// successfully quitting the old app.
|
||||||
|
params.nextAppName = nextAppName
|
||||||
|
params.nextSession = appModel.createSessionForApp(nextAppIndex)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
params.nextAppName = null
|
||||||
|
params.nextSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
|
appModel.quitRunningApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import StreamingPreferences 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
@@ -13,11 +15,10 @@ GridView {
|
|||||||
id: pcGrid
|
id: pcGrid
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 5
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 350; cellHeight: 350;
|
cellWidth: 350; cellHeight: 350;
|
||||||
focus: true
|
|
||||||
objectName: "Computers"
|
objectName: "Computers"
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
@@ -25,19 +26,19 @@ GridView {
|
|||||||
// routine to run, but only if we start as invisible
|
// routine to run, but only if we start as invisible
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
|
StreamingPreferences {
|
||||||
|
id: prefs
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
if (prefs.isRunningWayland()) {
|
||||||
if (visible) {
|
waylandDialog.open()
|
||||||
// Start polling when this view is shown
|
|
||||||
ComputerManager.startPolling()
|
|
||||||
}
|
}
|
||||||
else {
|
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||||
// Stop polling when this view is not top-most
|
noHwDecoderDialog.open()
|
||||||
ComputerManager.stopPollingAsync()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,11 +47,8 @@ GridView {
|
|||||||
// Close the PIN dialog
|
// Close the PIN dialog
|
||||||
pairDialog.close()
|
pairDialog.close()
|
||||||
|
|
||||||
// Start polling again
|
|
||||||
ComputerManager.startPolling()
|
|
||||||
|
|
||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
if (error !== null) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
@@ -59,7 +57,7 @@ GridView {
|
|||||||
function addComplete(success)
|
function addComplete(success)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience."
|
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,14 +70,14 @@ GridView {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
model: createModel()
|
model: computerModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ItemDelegate {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
source: {
|
source: {
|
||||||
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
||||||
}
|
}
|
||||||
@@ -89,6 +87,30 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
// TODO: Tooltip
|
||||||
|
id: stateIcon
|
||||||
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -10
|
||||||
|
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
||||||
|
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: statusUnknownSpinner
|
||||||
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: -10
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
visible: !model.addPc && model.statusUnknown
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: pcNameText
|
id: pcNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
@@ -101,76 +123,64 @@ GridView {
|
|||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Menu {
|
||||||
function getStatusText(model)
|
id: pcContextMenu
|
||||||
{
|
MenuItem {
|
||||||
if (model.online) {
|
text: "Wake PC"
|
||||||
var text = "<font color=\"green\">Online</font>"
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
text += "<font color=\"white\"> - </font>"
|
visible: !model.addPc && !model.online && model.wakeable
|
||||||
if (model.paired) {
|
height: visible ? implicitHeight : 0
|
||||||
text += "<font color=\"skyblue\">Paired</font>"
|
}
|
||||||
}
|
MenuItem {
|
||||||
else if (model.busy) {
|
text: "Delete PC"
|
||||||
text += "<font color=\"red\">Busy</font>"
|
onTriggered: {
|
||||||
}
|
deletePcDialog.pcIndex = index
|
||||||
else {
|
// get confirmation first, actual closing is called from the dialog
|
||||||
text += "<font color=\"red\">Not Paired</font>"
|
deletePcDialog.open()
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return "<font color=\"red\">Offline</font>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
id: pcPairedText
|
onClicked: {
|
||||||
text: getStatusText(model)
|
if (model.addPc) {
|
||||||
visible: !model.addPc
|
addPcDialog.open()
|
||||||
|
}
|
||||||
|
else if (model.online) {
|
||||||
|
if (model.paired) {
|
||||||
|
// go to game view
|
||||||
|
var component = Qt.createComponent("AppView.qml")
|
||||||
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||||
|
stackView.push(appView)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!model.busy) {
|
||||||
|
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||||
|
|
||||||
width: parent.width
|
// Kick off pairing in the background
|
||||||
anchors.top: pcNameText.bottom
|
computerModel.pairComputer(index, pin)
|
||||||
font.pointSize: 24
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
// Display the pairing dialog
|
||||||
|
pairDialog.pin = pin
|
||||||
|
pairDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// cannot pair while something is streaming or attempting to pair
|
||||||
|
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!model.online) {
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.addPc) {
|
// right click
|
||||||
addPcDialog.open()
|
if (!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||||
}
|
pcContextMenu.open()
|
||||||
else if (model.online) {
|
|
||||||
if (model.paired) {
|
|
||||||
// go to game view
|
|
||||||
var component = Qt.createComponent("AppView.qml")
|
|
||||||
var appView = component.createObject(stackView)
|
|
||||||
appView.computerIndex = index
|
|
||||||
appView.objectName = model.name
|
|
||||||
stackView.push(appView)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!model.busy) {
|
|
||||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
|
||||||
|
|
||||||
// Stop polling, since pairing may make GFE unresponsive
|
|
||||||
ComputerManager.stopPollingAsync()
|
|
||||||
|
|
||||||
// Kick off pairing in the background
|
|
||||||
computerModel.pairComputer(index, pin)
|
|
||||||
|
|
||||||
// Display the pairing dialog
|
|
||||||
pairDialog.pin = pin
|
|
||||||
pairDialog.open()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// cannot pair while something is streaming or attempting to pair
|
|
||||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
|
||||||
errorDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO: Wake on LAN and delete PC options
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +191,37 @@ GridView {
|
|||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
modality:Qt.WindowModal
|
modality:Qt.WindowModal
|
||||||
icon: StandardIcon.Critical
|
icon: StandardIcon.Critical
|
||||||
standardButtons: StandardButton.Ok
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
onHelp: {
|
||||||
|
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||||
|
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: noHwDecoderDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||||
|
"Your streaming performance may be severely degraded in this configuration. " +
|
||||||
|
"Click the Help button for more information on solving this problem."
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: waylandDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
||||||
|
"Please switch to an X session for optimal performance."
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
MessageDialog {
|
||||||
@@ -196,6 +236,19 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: deletePcDialog
|
||||||
|
// don't allow edits to the rest of the window while open
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
property int pcIndex : -1;
|
||||||
|
text:"Are you sure you want to remove this PC?"
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: {
|
||||||
|
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||||
|
computerModel.deleteComputer(pcIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
id: addPcDialog
|
id: addPcDialog
|
||||||
property string label: "Enter the IP address of your GameStream PC"
|
property string label: "Enter the IP address of your GameStream PC"
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import Session 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property string appName
|
||||||
|
property Session nextSession : null
|
||||||
|
property string nextAppName : ""
|
||||||
|
|
||||||
|
property string stageText : "Quitting " + appName + "..."
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
// The StackView will trigger a visibility change when
|
||||||
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
|
// routine to run, but only if we start as invisible
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
function quitAppCompleted(error)
|
||||||
|
{
|
||||||
|
// Display a failed dialog if we got an error
|
||||||
|
if (error !== undefined) {
|
||||||
|
errorDialog.text = error
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit this view
|
||||||
|
stackView.pop()
|
||||||
|
|
||||||
|
// If we're supposed to launch another game after this, do so now
|
||||||
|
if (error === undefined && nextSession !== null) {
|
||||||
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
|
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||||
|
stackView.push(segue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
// Connect the quit completion signal
|
||||||
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Disconnect the signal
|
||||||
|
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: stageSpinner
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: stageLabel
|
||||||
|
height: stageSpinner.height
|
||||||
|
text: stageText
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: errorDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Critical
|
||||||
|
standardButtons: StandardButton.Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import ComputerManager 1.0
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
@@ -16,14 +17,13 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
x: 10
|
padding: 10
|
||||||
y: 10
|
id: settingsColumn1
|
||||||
width: settingsPage.width
|
width: settingsPage.width / 2 - padding
|
||||||
height: 400
|
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: basicSettingsGroupBox
|
id: basicSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - 2 * parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Basic Settings</font>"
|
title: "<font color=\"skyblue\">Basic Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -35,7 +35,7 @@ ScrollView {
|
|||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPStitle
|
id: resFPStitle
|
||||||
text: qsTr("Resolution and FPS target")
|
text: qsTr("Resolution and FPS")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -44,84 +44,208 @@ ScrollView {
|
|||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPSdesc
|
id: resFPSdesc
|
||||||
text: qsTr("Setting values too high for your device may cause lag or crashes")
|
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
Row {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
spacing: 5
|
||||||
Component.onCompleted: {
|
|
||||||
// load the saved width/height/fps, and iterate through the ComboBox until a match is found
|
ComboBox {
|
||||||
// set it to that index.
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
var saved_width = prefs.width
|
Component.onCompleted: {
|
||||||
var saved_height = prefs.height
|
// Add native resolutions for all attached displays
|
||||||
var saved_fps = prefs.fps
|
var done = false
|
||||||
currentIndex = 0
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
for(var i = 0; i < resolutionComboBox.count; i++) {
|
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
var screenRect;
|
||||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
|
||||||
var el_fps = parseInt(resolutionListModel.get(i).video_fps);
|
// Some platforms have different desktop resolutions
|
||||||
if(saved_width === el_width &&
|
// and native resolutions (like macOS with Retina displays)
|
||||||
saved_height === el_height &&
|
if (displayResIndex == 0) {
|
||||||
saved_fps === el_fps) {
|
screenRect = prefs.getDesktopResolution(displayIndex)
|
||||||
currentIndex = i
|
}
|
||||||
}
|
else {
|
||||||
|
screenRect = prefs.getNativeResolution(displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenRect.width === 0) {
|
||||||
|
// Exceeded max count of displays
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexToAdd = 0
|
||||||
|
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||||
|
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||||
|
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||||
|
|
||||||
|
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||||
|
// Duplicate entry, skip
|
||||||
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert this display's resolution if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
resolutionListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||||
|
"video_width": ""+screenRect.width,
|
||||||
|
"video_height": ""+screenRect.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||||
|
// and set it to that index.
|
||||||
|
var saved_width = prefs.width
|
||||||
|
var saved_height = prefs.height
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||||
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
|
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||||
|
|
||||||
|
// Pick the highest value lesser or equal to the saved resolution
|
||||||
|
if (saved_width * saved_height >= el_width * el_height) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
id: resolutionComboBox
|
||||||
|
width: 200
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: resolutionListModel
|
||||||
|
// Other elements may be added at runtime
|
||||||
|
// based on attached display resolution
|
||||||
|
ListElement {
|
||||||
|
text: "720p"
|
||||||
|
video_width: "1280"
|
||||||
|
video_height: "720"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "1080p"
|
||||||
|
video_width: "1920"
|
||||||
|
video_height: "1080"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "1440p"
|
||||||
|
video_width: "2560"
|
||||||
|
video_height: "1440"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "4K"
|
||||||
|
video_width: "3840"
|
||||||
|
video_height: "2160"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||||
|
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
|
// Only modify the bitrate if the values actually changed
|
||||||
|
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
||||||
|
prefs.width = selectedWidth
|
||||||
|
prefs.height = selectedHeight
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
ComboBox {
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
function createModel() {
|
||||||
font.pointSize: 9
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
textRole: "text"
|
|
||||||
model: ListModel {
|
|
||||||
id: resolutionListModel
|
|
||||||
ListElement {
|
|
||||||
text: "720p 30 FPS"
|
|
||||||
video_width: "1280"
|
|
||||||
video_height: "720"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "720p 60 FPS"
|
|
||||||
video_width: "1280"
|
|
||||||
video_height: "720"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "1080p 30 FPS"
|
|
||||||
video_width: "1920"
|
|
||||||
video_height: "1080"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "1080p 60 FPS"
|
|
||||||
video_width: "1920"
|
|
||||||
video_height: "1080"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "4K 30 FPS"
|
|
||||||
video_width: "3840"
|
|
||||||
video_height: "2160"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "4K 60 FPS"
|
|
||||||
video_width: "3840"
|
|
||||||
video_height: "2160"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
|
||||||
onActivated : {
|
|
||||||
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
|
||||||
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
|
||||||
prefs.fps = parseInt(resolutionListModel.get(currentIndex).video_fps)
|
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
// Get the max supported FPS on this system
|
||||||
slider.value = prefs.bitrateKbps
|
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
|
// Default entries
|
||||||
|
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||||
|
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come before the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps > 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use 64 as the cutoff for adding a separate option to
|
||||||
|
// handle wonky displays that report just over 60 Hz.
|
||||||
|
if (max_fps > 64) {
|
||||||
|
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps < 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps < 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fpsListModel
|
||||||
|
}
|
||||||
|
|
||||||
|
function reinitialize() {
|
||||||
|
model = createModel()
|
||||||
|
|
||||||
|
var saved_fps = prefs.fps
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < model.count; i++) {
|
||||||
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
|
|
||||||
|
// Pick the highest value lesser or equal to the saved FPS
|
||||||
|
if (saved_fps >= el_fps) {
|
||||||
|
currentIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
reinitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
id: fpsComboBox
|
||||||
|
textRole: "text"
|
||||||
|
width: 250
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
|
|
||||||
|
// Only modify the bitrate if the values actually changed
|
||||||
|
if (prefs.fps !== selectedFps) {
|
||||||
|
prefs.fps = selectedFps
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +275,7 @@ ScrollView {
|
|||||||
|
|
||||||
stepSize: 500
|
stepSize: 500
|
||||||
from : 500
|
from : 500
|
||||||
to: 100000
|
to: 150000
|
||||||
|
|
||||||
snapMode: "SnapOnRelease"
|
snapMode: "SnapOnRelease"
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
@@ -162,13 +286,60 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
id: windowModeTitle
|
||||||
|
text: qsTr("Display mode")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var savedWm = prefs.windowMode
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
|
if (savedWm === thisWm) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
id: windowModeComboBox
|
||||||
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: windowModeListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Full-screen (Recommended)"
|
||||||
|
val: StreamingPreferences.WM_FULLSCREEN
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Borderless windowed"
|
||||||
|
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Windowed"
|
||||||
|
val: StreamingPreferences.WM_WINDOWED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActivated: {
|
||||||
|
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: fullScreenCheck
|
id: vsyncCheck
|
||||||
text: "<font color=\"white\">Full-screen</font>"
|
text: "<font color=\"white\">Enable V-Sync</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.fullScreen
|
checked: prefs.enableVsync
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.fullScreen = checked
|
prefs.enableVsync = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,7 +348,7 @@ ScrollView {
|
|||||||
GroupBox {
|
GroupBox {
|
||||||
|
|
||||||
id: audioSettingsGroupBox
|
id: audioSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - 2 * parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Audio Settings</font>"
|
title: "<font color=\"skyblue\">Audio Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -186,29 +357,72 @@ ScrollView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
Label {
|
||||||
id: surroundSoundCheck
|
width: parent.width
|
||||||
text: "<font color=\"white\">Enable 5.1 surround sound</font>"
|
id: resAudioTitle
|
||||||
font.pointSize: 12
|
text: qsTr("Audio configuration")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
// the backend actually supports auto/stereo/5.1. We'll expose stereo/5.1
|
ComboBox {
|
||||||
checked: prefs.audioConfig === StreamingPreferences.AC_FORCE_SURROUND
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var saved_audio = prefs.audioConfig
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < audioListModel.count; i++) {
|
||||||
|
var el_audio = audioListModel.get(i).val;
|
||||||
|
if (saved_audio === el_audio) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
onCheckedChanged: {
|
id: audioComboBox
|
||||||
prefs.audioConfig = checked ? StreamingPreferences.AC_FORCE_SURROUND : StreamingPreferences.AC_FORCE_STEREO
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: audioListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Autodetect"
|
||||||
|
val: StreamingPreferences.AC_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Stereo"
|
||||||
|
val: StreamingPreferences.AC_FORCE_STEREO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "5.1 surround sound"
|
||||||
|
val: StreamingPreferences.AC_FORCE_SURROUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.audioConfig = audioListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
padding: 10
|
||||||
|
anchors.left: settingsColumn1.right
|
||||||
|
id: settingsColumn2
|
||||||
|
width: settingsPage.width / 2 - padding
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: gamepadSettingsGroupBox
|
id: gamepadSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
title: "<font color=\"skyblue\">Input Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
@@ -221,49 +435,35 @@ ScrollView {
|
|||||||
prefs.multiController = checked
|
prefs.multiController = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CheckBox {
|
|
||||||
id: mouseEmulationCheck
|
|
||||||
text: "<font color=\"white\">Mouse emulation via gamepad</font>"
|
|
||||||
font.pointSize: 12
|
|
||||||
// TODO: make this actually do anything
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupBox {
|
|
||||||
id: onScreenControlsGroupBox
|
|
||||||
width: (parent.width - 20)
|
|
||||||
padding: 12
|
|
||||||
title: "<font color=\"skyblue\">On-screen Controls Settings</font>"
|
|
||||||
font.pointSize: 12
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: onScreenControlsCheck
|
id: mouseAccelerationCheck
|
||||||
text: "<font color=\"white\">Show on-screen controls</font>"
|
text: "<font color=\"white\">Enable mouse acceleration</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
// TODO: make this actually do anything
|
checked: prefs.mouseAcceleration
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.mouseAcceleration = checked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: hostSettingsGroupBox
|
id: hostSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "<font color=\"white\">Optimize game settings</font>"
|
text: "<font color=\"white\">Optimize game settings</font>"
|
||||||
|
// HACK: Match width of the other checkbox to make the UI not look bad
|
||||||
|
width: multiControllerCheck.width
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.gameOptimizations
|
checked: prefs.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -285,7 +485,7 @@ ScrollView {
|
|||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: advancedSettingsGroupBox
|
id: advancedSettingsGroupBox
|
||||||
width: (parent.width - 20)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -294,10 +494,141 @@ ScrollView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
id: resVDSTitle
|
||||||
|
text: qsTr("Video decoder")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var saved_vds = prefs.videoDecoderSelection
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < decoderListModel.count; i++) {
|
||||||
|
var el_vds = decoderListModel.get(i).val;
|
||||||
|
if (saved_vds === el_vds) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
id: decoderComboBox
|
||||||
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: decoderListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Automatic (Recommended)"
|
||||||
|
val: StreamingPreferences.VDS_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force software decoding"
|
||||||
|
val: StreamingPreferences.VDS_FORCE_SOFTWARE
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force hardware decoding"
|
||||||
|
val: StreamingPreferences.VDS_FORCE_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
id: resVCCTitle
|
||||||
|
text: qsTr("Video codec")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var saved_vcc = prefs.videoCodecConfig
|
||||||
|
currentIndex = 0
|
||||||
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
|
var el_vcc = codecListModel.get(i).val;
|
||||||
|
if (saved_vcc === el_vcc) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
id: codecComboBox
|
||||||
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: codecListModel
|
||||||
|
ListElement {
|
||||||
|
text: "Automatic (Recommended)"
|
||||||
|
val: StreamingPreferences.VCC_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force H.264"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_H264
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Force HEVC"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||||
|
}
|
||||||
|
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
|
||||||
|
// we'll probably need to gate this feature on OS support in our
|
||||||
|
// renderers.
|
||||||
|
/* ListElement {
|
||||||
|
text: "Force HEVC HDR"
|
||||||
|
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: neverDropFramesCheck
|
id: unlockUnsupportedFps
|
||||||
text: "<font color=\"white\">Never drop frames</font>"
|
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
checked: prefs.unsupportedFps
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.unsupportedFps = checked
|
||||||
|
|
||||||
|
// The selectable FPS values depend on whether
|
||||||
|
// this option is enabled or not
|
||||||
|
fpsComboBox.reinitialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: enableMdns
|
||||||
|
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.enableMdns
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.enableMdns = checked
|
||||||
|
|
||||||
|
// We must save the updated preference to ensure
|
||||||
|
// ComputerManager can observe the change internally.
|
||||||
|
prefs.save()
|
||||||
|
|
||||||
|
// Restart polling so the mDNS change takes effect
|
||||||
|
if (window.pollingActive) {
|
||||||
|
ComputerManager.stopPollingAsync()
|
||||||
|
ComputerManager.startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property Session session
|
property Session session
|
||||||
property string appname
|
property string appName
|
||||||
property string stageText : "Starting " + appname + "..."
|
property string stageText : "Starting " + appName + "..."
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
@@ -47,7 +48,13 @@ Item {
|
|||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
{
|
{
|
||||||
// TODO: toast
|
// 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.3; ToolTip {}', parent, '')
|
||||||
|
toast.text = text
|
||||||
|
toast.timeout = 3000
|
||||||
|
toast.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
@@ -63,7 +70,7 @@ Item {
|
|||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec()
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
|
|
||||||
// Show the Qt window again after streaming
|
// Show the Qt window again after streaming
|
||||||
window.visible = true
|
window.visible = true
|
||||||
@@ -97,10 +104,25 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 50
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
font.pointSize: 18
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
MessageDialog {
|
MessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
modality:Qt.WindowModal
|
||||||
icon: StandardIcon.Critical
|
icon: StandardIcon.Critical
|
||||||
standardButtons: StandardButton.Ok
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,32 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
|||||||
m_CurrentGameId = m_Computer->currentGameId;
|
m_CurrentGameId = m_Computer->currentGameId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AppModel::getRunningAppIndex()
|
||||||
|
{
|
||||||
|
if (m_CurrentGameId != 0) {
|
||||||
|
for (int i = 0; i < m_Apps.count(); i++) {
|
||||||
|
if (m_Apps[i].id == m_CurrentGameId) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AppModel::getRunningAppName()
|
||||||
|
{
|
||||||
|
if (m_CurrentGameId != 0) {
|
||||||
|
for (int i = 0; i < m_Apps.count(); i++) {
|
||||||
|
if (m_Apps[i].id == m_CurrentGameId) {
|
||||||
|
return m_Apps[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
Session* AppModel::createSessionForApp(int appIndex)
|
Session* AppModel::createSessionForApp(int appIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(appIndex < m_Apps.count());
|
Q_ASSERT(appIndex < m_Apps.count());
|
||||||
@@ -70,6 +96,11 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AppModel::quitRunningApp()
|
||||||
|
{
|
||||||
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Ignore updates for computers that aren't ours
|
// Ignore updates for computers that aren't ours
|
||||||
@@ -77,6 +108,14 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the computer has gone offline or we've been unpaired,
|
||||||
|
// signal the UI so we can go back to the PC view.
|
||||||
|
if (m_Computer->state == NvComputer::CS_OFFLINE ||
|
||||||
|
m_Computer->pairState == NvComputer::PS_NOT_PAIRED) {
|
||||||
|
emit computerLost();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// First, process additions/removals from the app list. This
|
// First, process additions/removals from the app list. This
|
||||||
// is required because the new game may now be running, so
|
// is required because the new game may now be running, so
|
||||||
// we can't check that first.
|
// we can't check that first.
|
||||||
@@ -123,10 +162,15 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
|
|||||||
Q_ASSERT(computer == m_Computer);
|
Q_ASSERT(computer == m_Computer);
|
||||||
|
|
||||||
int index = m_Apps.indexOf(app);
|
int index = m_Apps.indexOf(app);
|
||||||
Q_ASSERT(index >= 0);
|
|
||||||
|
|
||||||
// Let our view know the box art data has changed for this app
|
// Make sure we're not delivering a callback to an app that's already been removed
|
||||||
emit dataChanged(createIndex(index, 0),
|
if (index >= 0) {
|
||||||
createIndex(index, 0),
|
// Let our view know the box art data has changed for this app
|
||||||
QVector<int>() << BoxArtRole);
|
emit dataChanged(createIndex(index, 0),
|
||||||
|
createIndex(index, 0),
|
||||||
|
QVector<int>() << BoxArtRole);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "App not found for box art callback:" << app.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,12 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE int getRunningAppIndex();
|
||||||
|
|
||||||
|
Q_INVOKABLE QString getRunningAppName();
|
||||||
|
|
||||||
|
Q_INVOKABLE void quitRunningApp();
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
@@ -36,6 +42,9 @@ private slots:
|
|||||||
|
|
||||||
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void computerLost();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
BoxArtManager m_BoxArtManager;
|
BoxArtManager m_BoxArtManager;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "computermodel.h"
|
#include "computermodel.h"
|
||||||
|
|
||||||
|
#include <QThreadPool>
|
||||||
|
|
||||||
ComputerModel::ComputerModel(QObject* object)
|
ComputerModel::ComputerModel(QObject* object)
|
||||||
: QAbstractListModel(object) {}
|
: QAbstractListModel(object) {}
|
||||||
|
|
||||||
@@ -46,8 +48,12 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return computer->pairState == NvComputer::PS_PAIRED;
|
return computer->pairState == NvComputer::PS_PAIRED;
|
||||||
case BusyRole:
|
case BusyRole:
|
||||||
return computer->currentGameId != 0;
|
return computer->currentGameId != 0;
|
||||||
|
case WakeableRole:
|
||||||
|
return !computer->macAddress.isEmpty();
|
||||||
case AddPcRole:
|
case AddPcRole:
|
||||||
return false;
|
return false;
|
||||||
|
case StatusUnknownRole:
|
||||||
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -74,6 +80,8 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[PairedRole] = "paired";
|
names[PairedRole] = "paired";
|
||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[AddPcRole] = "addPc";
|
names[AddPcRole] = "addPc";
|
||||||
|
names[WakeableRole] = "wakeable";
|
||||||
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
@@ -113,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ComputerModel::wakeComputer(int computerIndex)
|
class DeferredWakeHostTask : public QRunnable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeferredWakeHostTask(NvComputer* computer)
|
||||||
|
: m_Computer(computer) {}
|
||||||
|
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
m_Computer->wake();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ComputerModel::wakeComputer(int computerIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(computerIndex < m_Computers.count());
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
|
|
||||||
return m_Computers[computerIndex]->wake();
|
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
|
||||||
|
QThreadPool::globalInstance()->start(wakeTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||||
@@ -129,7 +153,7 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
|
|||||||
|
|
||||||
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||||
{
|
{
|
||||||
emit pairingCompleted(error.isNull() ? QVariant() : error);
|
emit pairingCompleted(error.isEmpty() ? QVariant() : error);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class ComputerModel : public QAbstractListModel
|
|||||||
OnlineRole,
|
OnlineRole,
|
||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
|
WakeableRole,
|
||||||
|
StatusUnknownRole,
|
||||||
AddPcRole
|
AddPcRole
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||||
|
|
||||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import QtQuick.Controls.Material 2.1
|
import QtQuick.Controls.Material 2.1
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import AutoUpdateChecker 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
|
property bool pollingActive: false
|
||||||
|
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
visible: true
|
||||||
width: 1280
|
width: 1280
|
||||||
@@ -19,6 +25,24 @@ ApplicationWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVisibilityChanged: {
|
||||||
|
// We don't want to just use 'active' here because that will stop polling if
|
||||||
|
// we lose focus, which might be overzealous for users with multiple screens
|
||||||
|
// where we may be clearly visible on the other display. Ideally we'll poll
|
||||||
|
// only if the window is visible to the user (not if obscured by other windows),
|
||||||
|
// but it seems difficult to do this portably.
|
||||||
|
var shouldPoll = visibility !== Window.Minimized && visibility !== Window.Hidden
|
||||||
|
|
||||||
|
if (shouldPoll && !pollingActive) {
|
||||||
|
ComputerManager.startPolling()
|
||||||
|
pollingActive = true
|
||||||
|
}
|
||||||
|
else if (!shouldPoll && pollingActive) {
|
||||||
|
ComputerManager.stopPollingAsync()
|
||||||
|
pollingActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(url, objectName)
|
function navigateTo(url, objectName)
|
||||||
{
|
{
|
||||||
var existingItem = stackView.find(function(item, index) {
|
var existingItem = stackView.find(function(item, index) {
|
||||||
@@ -37,6 +61,8 @@ ApplicationWindow {
|
|||||||
|
|
||||||
header: ToolBar {
|
header: ToolBar {
|
||||||
id: toolBar
|
id: toolBar
|
||||||
|
anchors.topMargin: 5
|
||||||
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
@@ -46,14 +72,19 @@ ApplicationWindow {
|
|||||||
// Only make the button visible if the user has navigated somewhere.
|
// Only make the button visible if the user has navigated somewhere.
|
||||||
visible: stackView.depth > 1
|
visible: stackView.depth > 1
|
||||||
|
|
||||||
icon.source: "qrc:/res/arrow_left.svg"
|
// FIXME: We're using an Image here rather than icon.source because
|
||||||
onClicked: stackView.pop()
|
// icons don't work on Qt 5.9 LTS.
|
||||||
|
Image {
|
||||||
Menu {
|
source: "qrc:/res/arrow_left.svg"
|
||||||
id: backButton
|
anchors.centerIn: parent
|
||||||
x: parent.width - width
|
sourceSize {
|
||||||
transformOrigin: Menu.TopRight
|
// The icon should be square so use the height as the width too
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClicked: stackView.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -67,38 +98,99 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/question_mark.svg"
|
property string browserUrl: ""
|
||||||
|
|
||||||
|
id: updateButton
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/update.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Update available for Moonlight"
|
||||||
|
|
||||||
|
// Invisible until we get a callback notifying us that
|
||||||
|
// an update is available
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
|
|
||||||
|
function updateAvailable(url)
|
||||||
|
{
|
||||||
|
updateButton.browserUrl = url
|
||||||
|
updateButton.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||||
|
AutoUpdateChecker.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/question_mark.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Help"
|
||||||
|
|
||||||
// TODO need to make sure browser is brought to foreground.
|
// TODO need to make sure browser is brought to foreground.
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: helpButton
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
// TODO: Implement gamepad mapping then unhide this button
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Gamepad Mapper"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: gamepadMappingMenu
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/settings.svg"
|
Image {
|
||||||
|
source: "qrc:/res/settings.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||||
|
|
||||||
Menu {
|
ToolTip.delay: 1000
|
||||||
id: optionsMenu
|
ToolTip.timeout: 3000
|
||||||
x: parent.width
|
ToolTip.visible: hovered
|
||||||
transformOrigin: Menu.TopRight
|
ToolTip.text: "Settings"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QtDebug>
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -9,23 +11,265 @@
|
|||||||
#define SDL_MAIN_HANDLED
|
#define SDL_MAIN_HANDLED
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
#include "streaming/video/ffmpeg.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "path.h"
|
||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
|
#include "backend/autoupdatechecker.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.hpp"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
|
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||||
|
// Log to file for release Windows builds
|
||||||
|
#define USE_CUSTOM_LOGGER
|
||||||
|
#define LOG_TO_FILE
|
||||||
|
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
|
||||||
|
// Use stdout logger on all Linux/BSD builds
|
||||||
|
#define USE_CUSTOM_LOGGER
|
||||||
|
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
|
||||||
|
// Log to file for release Mac builds
|
||||||
|
#define USE_CUSTOM_LOGGER
|
||||||
|
#define LOG_TO_FILE
|
||||||
|
#else
|
||||||
|
// For debug Windows and Mac builds, use default logger
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_CUSTOM_LOGGER
|
||||||
|
static QTime s_LoggerTime;
|
||||||
|
static QTextStream s_LoggerStream(stdout);
|
||||||
|
static QMutex s_LoggerLock;
|
||||||
|
#ifdef LOG_TO_FILE
|
||||||
|
#define MAX_LOG_LINES 10000
|
||||||
|
static int s_LogLinesWritten = 0;
|
||||||
|
static bool s_LogLimitReached = false;
|
||||||
|
static QFile* s_LoggerFile;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void logToLoggerStream(QString& message)
|
||||||
|
{
|
||||||
|
QMutexLocker lock(&s_LoggerLock);
|
||||||
|
|
||||||
|
#ifdef LOG_TO_FILE
|
||||||
|
if (s_LogLimitReached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
||||||
|
s_LoggerStream << "Log size limit reached!" << endl;
|
||||||
|
s_LogLimitReached = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s_LogLinesWritten++;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
s_LoggerStream << message;
|
||||||
|
s_LoggerStream.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||||
|
{
|
||||||
|
QString priorityTxt;
|
||||||
|
|
||||||
|
switch (priority) {
|
||||||
|
case SDL_LOG_PRIORITY_VERBOSE:
|
||||||
|
priorityTxt = "Verbose";
|
||||||
|
break;
|
||||||
|
case SDL_LOG_PRIORITY_DEBUG:
|
||||||
|
priorityTxt = "Debug";
|
||||||
|
break;
|
||||||
|
case SDL_LOG_PRIORITY_INFO:
|
||||||
|
priorityTxt = "Info";
|
||||||
|
break;
|
||||||
|
case SDL_LOG_PRIORITY_WARN:
|
||||||
|
priorityTxt = "Warn";
|
||||||
|
break;
|
||||||
|
case SDL_LOG_PRIORITY_ERROR:
|
||||||
|
priorityTxt = "Error";
|
||||||
|
break;
|
||||||
|
case SDL_LOG_PRIORITY_CRITICAL:
|
||||||
|
priorityTxt = "Critical";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
priorityTxt = "Unknown";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
|
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||||
|
|
||||||
|
logToLoggerStream(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
|
||||||
|
{
|
||||||
|
QString typeTxt;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg:
|
||||||
|
typeTxt = "Debug";
|
||||||
|
break;
|
||||||
|
case QtInfoMsg:
|
||||||
|
typeTxt = "Info";
|
||||||
|
break;
|
||||||
|
case QtWarningMsg:
|
||||||
|
typeTxt = "Warning";
|
||||||
|
break;
|
||||||
|
case QtCriticalMsg:
|
||||||
|
typeTxt = "Critical";
|
||||||
|
break;
|
||||||
|
case QtFatalMsg:
|
||||||
|
typeTxt = "Fatal";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||||
|
|
||||||
|
logToLoggerStream(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
|
||||||
|
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||||
|
{
|
||||||
|
char lineBuffer[1024];
|
||||||
|
static int printPrefix = 1;
|
||||||
|
|
||||||
|
if ((level & 0xFF) > av_log_get_level()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||||
|
|
||||||
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
|
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||||
|
|
||||||
|
logToLoggerStream(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <DbgHelp.h>
|
||||||
|
|
||||||
|
static UINT s_HitUnhandledException = 0;
|
||||||
|
|
||||||
|
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||||
|
{
|
||||||
|
// Only write a dump for the first unhandled exception
|
||||||
|
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR dmpFileName[MAX_PATH];
|
||||||
|
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||||
|
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||||
|
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||||
|
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||||
|
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||||
|
|
||||||
|
info.ThreadId = GetCurrentThreadId();
|
||||||
|
info.ExceptionPointers = ExceptionInfo;
|
||||||
|
info.ClientPointers = FALSE;
|
||||||
|
|
||||||
|
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
|
||||||
|
MiniDumpIgnoreInaccessibleMemory |
|
||||||
|
MiniDumpWithUnloadedModules |
|
||||||
|
MiniDumpWithThreadInfo;
|
||||||
|
|
||||||
|
if (MiniDumpWriteDump(GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
dumpHandle,
|
||||||
|
(MINIDUMP_TYPE)typeFlags,
|
||||||
|
&info,
|
||||||
|
nullptr,
|
||||||
|
nullptr)) {
|
||||||
|
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(dumpHandle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the program crash and WER collect a dump
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
// Set these here to allow us to use the default QSettings constructor.
|
||||||
|
// These also ensure that our cache directory is named correctly. As such,
|
||||||
|
// it is critical that these be called before Path::initialize().
|
||||||
|
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||||
|
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||||
|
QCoreApplication::setApplicationName("Moonlight");
|
||||||
|
|
||||||
|
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||||
|
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||||
|
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||||
|
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
|
||||||
|
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
|
||||||
|
|
||||||
|
// Initialize paths for portable mode
|
||||||
|
Path::initialize(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Initialize paths for standard installation
|
||||||
|
Path::initialize(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_CUSTOM_LOGGER
|
||||||
|
#ifdef LOG_TO_FILE
|
||||||
|
QDir tempDir(Path::getLogDir());
|
||||||
|
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||||
|
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
||||||
|
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
||||||
|
s_LoggerStream.setDevice(s_LoggerFile);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
s_LoggerTime.start();
|
||||||
|
qInstallMessageHandler(qtLogToDiskHandler);
|
||||||
|
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Create a crash dump when we crash on Windows
|
||||||
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
|
#endif
|
||||||
|
|
||||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||||
|
|
||||||
// Set these here to allow us to use the default QSettings constructor
|
#ifdef Q_OS_WIN32
|
||||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
// On Windows, use ANGLE so we don't have to load both DX and OGL
|
||||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
// user-mode drivers into our app.
|
||||||
QCoreApplication::setApplicationName("Moonlight");
|
qputenv("QT_OPENGL", "angle");
|
||||||
|
#endif
|
||||||
|
|
||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
@@ -44,6 +288,11 @@ int main(int argc, char *argv[])
|
|||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
return new ComputerManager();
|
return new ComputerManager();
|
||||||
});
|
});
|
||||||
|
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
|
||||||
|
"AutoUpdateChecker",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new AutoUpdateChecker();
|
||||||
|
});
|
||||||
|
|
||||||
QQuickStyle::setStyle("Material");
|
QQuickStyle::setStyle("Material");
|
||||||
|
|
||||||
@@ -53,17 +302,18 @@ int main(int argc, char *argv[])
|
|||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
// Ensure that SDL is always initialized since we may need to use it
|
|
||||||
// for non-streaming purposes (like checking on audio devices)
|
|
||||||
SDL_SetMainReady();
|
SDL_SetMainReady();
|
||||||
if (SDL_Init(SDL_INIT_VIDEO |
|
if (SDL_Init(0) != 0) {
|
||||||
SDL_INIT_AUDIO |
|
|
||||||
SDL_INIT_GAMECONTROLLER) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_Init() failed: %s",
|
"SDL_Init() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||||
|
// We don't want this all the time that Moonlight is open. We will set
|
||||||
|
// it manually when we start streaming.
|
||||||
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
|||||||
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,77 @@
|
|||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QtDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
QString Path::s_LogDir;
|
||||||
|
QString Path::s_BoxArtCacheDir;
|
||||||
|
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
||||||
|
|
||||||
|
QString Path::getLogDir()
|
||||||
|
{
|
||||||
|
Q_ASSERT(!s_LogDir.isEmpty());
|
||||||
|
return s_LogDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Path::getBoxArtCacheDir()
|
||||||
|
{
|
||||||
|
Q_ASSERT(!s_BoxArtCacheDir.isEmpty());
|
||||||
|
return s_BoxArtCacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Path::getGamepadMappingFile()
|
||||||
|
{
|
||||||
|
QString candidatePath;
|
||||||
|
|
||||||
|
// Check the current directory first
|
||||||
|
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now check the data directories (for Linux, in particular)
|
||||||
|
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
|
||||||
|
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
||||||
|
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try the Resources folder in our app bundle for macOS
|
||||||
|
QDir dir = QDir(QCoreApplication::applicationDirPath());
|
||||||
|
dir.cdUp();
|
||||||
|
dir.cd("Resources");
|
||||||
|
dir.filePath(k_GameControllerMappingFile);
|
||||||
|
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't find it
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Path::initialize(bool portable)
|
||||||
|
{
|
||||||
|
if (portable) {
|
||||||
|
s_LogDir = QDir::currentPath();
|
||||||
|
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
|
||||||
|
// easily find, so use the system's global tmp directory instead.
|
||||||
|
s_LogDir = "/tmp";
|
||||||
|
#else
|
||||||
|
s_LogDir = QDir::tempPath();
|
||||||
|
#endif
|
||||||
|
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class Path
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QString getLogDir();
|
||||||
|
|
||||||
|
static QString getBoxArtCacheDir();
|
||||||
|
|
||||||
|
static QString getGamepadMappingFile();
|
||||||
|
|
||||||
|
static void initialize(bool portable);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString s_LogDir;
|
||||||
|
static QString s_BoxArtCacheDir;
|
||||||
|
static const QString k_GameControllerMappingFile;
|
||||||
|
};
|
||||||
@@ -6,5 +6,6 @@
|
|||||||
<file>gui/SettingsView.qml</file>
|
<file>gui/SettingsView.qml</file>
|
||||||
<file>gui/StreamSegue.qml</file>
|
<file>gui/StreamSegue.qml</file>
|
||||||
<file>gui/GamepadMapper.qml</file>
|
<file>gui/GamepadMapper.qml</file>
|
||||||
|
<file>gui/QuitSegue.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 390 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
|
||||||
|
<path d="M0 0h48v48H0z" fill="none"/>
|
||||||
|
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 215 B |
@@ -1,23 +1,69 @@
|
|||||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<defs>
|
<svg
|
||||||
<filter id="f1" y="-40%" width="120%" height="220%">
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
<feOffset xmlns="http://www.w3.org/2000/svg" result="offOut" in="SourceAlpha" dy="-100"/>
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="200"/>
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
<feBlend in="SourceGraphic" in2="blurOut" mode="normal"/>
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
</filter>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</defs>
|
id="svg3761"
|
||||||
<!-- -105, 521 are within 0.5 pixel-perfect -->
|
viewBox="0 0 680 680"
|
||||||
<!-- offsetting more to show shadow -->
|
height="680"
|
||||||
<g transform="translate(-95,530) scale(0.100000,-0.100000)" filter="url(#f1)">
|
width="680"
|
||||||
<circle cx="4755" cy="1505" r="3700" fill="#565C64">
|
version="1.0">
|
||||||
</circle>
|
<metadata
|
||||||
<path d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" style="fill:#ffffff"/>
|
id="metadata3767">
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" style="fill:#ffffff"/>
|
<rdf:RDF>
|
||||||
<path d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" style="fill:#ffffff"/>
|
<cc:Work
|
||||||
<path d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" style="fill:#ffffff"/>
|
rdf:about="">
|
||||||
<path d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" style="fill:#ffffff"/>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<path d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" style="fill:#ffffff"/>
|
<dc:type
|
||||||
<path d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" style="fill:#ffffff"/>
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<path d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" style="fill:#ffffff"/>
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs3765" />
|
||||||
|
<g
|
||||||
|
id="g3759"
|
||||||
|
transform="translate(-135,491) scale(0.100000,-0.100000)">
|
||||||
|
<circle
|
||||||
|
id="circle3741"
|
||||||
|
fill="#565C64"
|
||||||
|
r="3400"
|
||||||
|
cy="1505"
|
||||||
|
cx="4755" />
|
||||||
|
<path
|
||||||
|
id="path3743"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
|
||||||
|
<path
|
||||||
|
id="path3745"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
|
||||||
|
<path
|
||||||
|
id="path3747"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
|
||||||
|
<path
|
||||||
|
id="path3749"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
|
||||||
|
<path
|
||||||
|
id="path3751"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
|
||||||
|
<path
|
||||||
|
id="path3753"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
|
||||||
|
<path
|
||||||
|
id="path3755"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
|
||||||
|
<path
|
||||||
|
id="path3757"
|
||||||
|
style="fill:#ffffff"
|
||||||
|
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||||
|
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
@@ -5,10 +5,14 @@
|
|||||||
<file>res/ic_videogame_asset_white_48px.svg</file>
|
<file>res/ic_videogame_asset_white_48px.svg</file>
|
||||||
<file>res/ic_tv_white_48px.svg</file>
|
<file>res/ic_tv_white_48px.svg</file>
|
||||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||||
|
<file>res/baseline-lock-24px.svg</file>
|
||||||
|
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||||
|
<file>res/baseline-warning-24px.svg</file>
|
||||||
<file>res/no_app_image.png</file>
|
<file>res/no_app_image.png</file>
|
||||||
<file>res/settings.svg</file>
|
<file>res/settings.svg</file>
|
||||||
<file>res/arrow_left.svg</file>
|
<file>res/arrow_left.svg</file>
|
||||||
<file>res/question_mark.svg</file>
|
<file>res/question_mark.svg</file>
|
||||||
<file>res/moonlight.svg</file>
|
<file>res/moonlight.svg</file>
|
||||||
|
<file>res/update.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
|
#include "streaming/session.hpp"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -8,12 +10,17 @@
|
|||||||
#define SER_FPS "fps"
|
#define SER_FPS "fps"
|
||||||
#define SER_BITRATE "bitrate"
|
#define SER_BITRATE "bitrate"
|
||||||
#define SER_FULLSCREEN "fullscreen"
|
#define SER_FULLSCREEN "fullscreen"
|
||||||
|
#define SER_VSYNC "vsync"
|
||||||
#define SER_GAMEOPTS "gameopts"
|
#define SER_GAMEOPTS "gameopts"
|
||||||
#define SER_HOSTAUDIO "hostaudio"
|
#define SER_HOSTAUDIO "hostaudio"
|
||||||
#define SER_MULTICONT "multicontroller"
|
#define SER_MULTICONT "multicontroller"
|
||||||
#define SER_AUDIOCFG "audiocfg"
|
#define SER_AUDIOCFG "audiocfg"
|
||||||
#define SER_VIDEOCFG "videocfg"
|
#define SER_VIDEOCFG "videocfg"
|
||||||
#define SER_VIDEODEC "videodec"
|
#define SER_VIDEODEC "videodec"
|
||||||
|
#define SER_WINDOWMODE "windowmode"
|
||||||
|
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||||
|
#define SER_MDNS "mdns"
|
||||||
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences()
|
StreamingPreferences::StreamingPreferences()
|
||||||
{
|
{
|
||||||
@@ -28,16 +35,23 @@ void StreamingPreferences::reload()
|
|||||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||||
fps = settings.value(SER_FPS, 60).toInt();
|
fps = settings.value(SER_FPS, 60).toInt();
|
||||||
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
||||||
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
|
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
||||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||||
|
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||||
|
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||||
|
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
|
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
||||||
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
||||||
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
||||||
|
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||||
|
// Try to load from the old preference value too
|
||||||
|
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||||
|
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamingPreferences::save()
|
void StreamingPreferences::save()
|
||||||
@@ -48,33 +62,146 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_HEIGHT, height);
|
settings.setValue(SER_HEIGHT, height);
|
||||||
settings.setValue(SER_FPS, fps);
|
settings.setValue(SER_FPS, fps);
|
||||||
settings.setValue(SER_BITRATE, bitrateKbps);
|
settings.setValue(SER_BITRATE, bitrateKbps);
|
||||||
settings.setValue(SER_FULLSCREEN, fullScreen);
|
settings.setValue(SER_VSYNC, enableVsync);
|
||||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||||
settings.setValue(SER_MULTICONT, multiController);
|
settings.setValue(SER_MULTICONT, multiController);
|
||||||
|
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||||
|
settings.setValue(SER_MDNS, enableMdns);
|
||||||
|
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||||
|
{
|
||||||
|
// Always use VDS_AUTO to avoid spamming the user with warnings
|
||||||
|
// if they've forced software decoding.
|
||||||
|
return Session::isHardwareDecodeAvailable(VDS_AUTO,
|
||||||
|
VIDEO_FORMAT_H264,
|
||||||
|
1920, 1080, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamingPreferences::isRunningWayland()
|
||||||
|
{
|
||||||
|
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
||||||
|
}
|
||||||
|
|
||||||
|
int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||||
|
{
|
||||||
|
// Never let the maximum drop below 60 FPS
|
||||||
|
int maxFrameRate = 60;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return maxFrameRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode, bestMode, desktopMode;
|
||||||
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
|
// Get the native desktop resolution
|
||||||
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
|
||||||
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
||||||
|
// to max out and drop frames.
|
||||||
|
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
return maxFrameRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
if (err == 0) {
|
||||||
|
return QRect(0, 0, mode.w, mode.h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return QRect(0, 0, mode.w, mode.h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
if (width * height * fps <= 1280 * 720 * 30) {
|
// This table prefers 16:10 resolutions because they are
|
||||||
return 5000;
|
// only slightly more pixels than the 16:9 equivalents, so
|
||||||
|
// we don't want to bump those 16:10 resolutions up to the
|
||||||
|
// next 16:9 slot.
|
||||||
|
|
||||||
|
// This covers 1280x720 and 1280x800 too
|
||||||
|
if (width * height <= 1366 * 768) {
|
||||||
|
return static_cast<int>(5000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1280 * 720 * 60) {
|
else if (width * height <= 1920 * 1200) {
|
||||||
return 10000;
|
return static_cast<int>(10000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 30) {
|
else if (width * height <= 2560 * 1600) {
|
||||||
return 10000;
|
return static_cast<int>(20000 * (fps / 30.0));
|
||||||
}
|
}
|
||||||
else if (width * height * fps <= 1920 * 1080 * 60) {
|
else /* if (width * height <= 3840 * 2160) */ {
|
||||||
return 20000;
|
return static_cast<int>(40000 * (fps / 30.0));
|
||||||
}
|
|
||||||
else if (width * height * fps <= 3840 * 2160 * 30) {
|
|
||||||
return 40000;
|
|
||||||
}
|
|
||||||
else /* if (width * height * fps <= 3840 * 2160 * 60) */ {
|
|
||||||
return 80000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
class StreamingPreferences : public QObject
|
class StreamingPreferences : public QObject
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,16 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool isRunningWayland();
|
||||||
|
|
||||||
|
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
@@ -41,40 +52,60 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(VideoDecoderSelection)
|
Q_ENUM(VideoDecoderSelection)
|
||||||
|
|
||||||
|
enum WindowMode
|
||||||
|
{
|
||||||
|
WM_FULLSCREEN,
|
||||||
|
WM_FULLSCREEN_DESKTOP,
|
||||||
|
WM_WINDOWED
|
||||||
|
};
|
||||||
|
Q_ENUM(WindowMode)
|
||||||
|
|
||||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
||||||
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
|
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
||||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||||
|
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||||
|
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||||
|
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int fps;
|
int fps;
|
||||||
int bitrateKbps;
|
int bitrateKbps;
|
||||||
bool fullScreen;
|
bool enableVsync;
|
||||||
bool gameOptimizations;
|
bool gameOptimizations;
|
||||||
bool playAudioOnHost;
|
bool playAudioOnHost;
|
||||||
bool multiController;
|
bool multiController;
|
||||||
|
bool unsupportedFps;
|
||||||
|
bool enableMdns;
|
||||||
|
bool mouseAcceleration;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
|
WindowMode windowMode;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayModeChanged();
|
void displayModeChanged();
|
||||||
void bitrateChanged();
|
void bitrateChanged();
|
||||||
void fullScreenChanged();
|
void enableVsyncChanged();
|
||||||
void gameOptimizationsChanged();
|
void gameOptimizationsChanged();
|
||||||
void playAudioOnHostChanged();
|
void playAudioOnHostChanged();
|
||||||
void multiControllerChanged();
|
void multiControllerChanged();
|
||||||
|
void unsupportedFpsChanged();
|
||||||
|
void enableMdnsChanged();
|
||||||
|
void mouseAccelerationChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
|
void windowModeChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
#include "session.hpp"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
#include <SDL.h>
|
|
||||||
|
|
||||||
#define MAX_CHANNELS 6
|
|
||||||
#define SAMPLES_PER_FRAME 240
|
|
||||||
#define MIN_QUEUED_FRAMES 2
|
|
||||||
#define MAX_QUEUED_FRAMES 4
|
|
||||||
#define STOP_THE_WORLD_LIMIT 20
|
|
||||||
#define DROP_RATIO_DENOM 32
|
|
||||||
|
|
||||||
SDL_AudioDeviceID Session::s_AudioDevice;
|
|
||||||
OpusMSDecoder* Session::s_OpusDecoder;
|
|
||||||
short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
|
||||||
int Session::s_ChannelCount;
|
|
||||||
int Session::s_PendingDrops;
|
|
||||||
int Session::s_PendingHardDrops;
|
|
||||||
unsigned int Session::s_SampleIndex;
|
|
||||||
Uint32 Session::s_BaselinePendingData;
|
|
||||||
|
|
||||||
int Session::sdlDetermineAudioConfiguration()
|
|
||||||
{
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
SDL_AudioDeviceID dev;
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = 48000;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = 6;
|
|
||||||
want.samples = 1024;
|
|
||||||
|
|
||||||
// Try to open for 5.1 surround sound, but allow SDL to tell us that's
|
|
||||||
// not available.
|
|
||||||
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
|
|
||||||
if (dev == 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open audio device");
|
|
||||||
// We'll probably have issues during audio stream init, but we'll
|
|
||||||
// try anyway
|
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_CloseAudioDevice(dev);
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio device has %d channels", have.channels);
|
|
||||||
|
|
||||||
if (have.channels > 2) {
|
|
||||||
// We don't support quadraphonic or 7.1 surround, but SDL
|
|
||||||
// should be able to downmix or upmix better from 5.1 than
|
|
||||||
// from stereo, so use 5.1 in non-stereo cases.
|
|
||||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int Session::sdlAudioInit(int /* audioConfiguration */,
|
|
||||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
|
||||||
void* /* arContext */, int /* arFlags */)
|
|
||||||
{
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
int error;
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = opusConfig->sampleRate;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = opusConfig->channelCount;
|
|
||||||
|
|
||||||
// This is supposed to be a power of 2, but our
|
|
||||||
// frames contain a non-power of 2 number of samples,
|
|
||||||
// so the slop would require buffering another full frame.
|
|
||||||
// Specifying non-Po2 seems to work for our supported platforms.
|
|
||||||
want.samples = SAMPLES_PER_FRAME;
|
|
||||||
|
|
||||||
s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
|
||||||
if (s_AudioDevice == 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open audio device: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
|
||||||
opusConfig->channelCount,
|
|
||||||
opusConfig->streams,
|
|
||||||
opusConfig->coupledStreams,
|
|
||||||
opusConfig->mapping,
|
|
||||||
&error);
|
|
||||||
if (s_OpusDecoder == NULL) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create decoder: %d",
|
|
||||||
error);
|
|
||||||
SDL_CloseAudioDevice(s_AudioDevice);
|
|
||||||
s_AudioDevice = 0;
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDL counts pending samples in the queued
|
|
||||||
// audio size using the WASAPI backend. This
|
|
||||||
// includes silence, which can throw off our
|
|
||||||
// pending data count. Get a baseline so we
|
|
||||||
// can exclude that data.
|
|
||||||
s_BaselinePendingData = 0;
|
|
||||||
#ifdef _WIN32
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
|
|
||||||
SDL_Delay(10);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
s_BaselinePendingData *= 2;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Baseline pending audio data: %d bytes",
|
|
||||||
s_BaselinePendingData);
|
|
||||||
|
|
||||||
s_ChannelCount = opusConfig->channelCount;
|
|
||||||
s_SampleIndex = 0;
|
|
||||||
s_PendingDrops = s_PendingHardDrops = 0;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioStart()
|
|
||||||
{
|
|
||||||
// Unpause the audio device
|
|
||||||
SDL_PauseAudioDevice(s_AudioDevice, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioStop()
|
|
||||||
{
|
|
||||||
// Pause the audio device
|
|
||||||
SDL_PauseAudioDevice(s_AudioDevice, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioCleanup()
|
|
||||||
{
|
|
||||||
SDL_CloseAudioDevice(s_AudioDevice);
|
|
||||||
s_AudioDevice = 0;
|
|
||||||
|
|
||||||
opus_multistream_decoder_destroy(s_OpusDecoder);
|
|
||||||
s_OpusDecoder = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|
||||||
{
|
|
||||||
int samplesDecoded;
|
|
||||||
|
|
||||||
s_SampleIndex++;
|
|
||||||
|
|
||||||
Uint32 queuedAudio = qMax((int)SDL_GetQueuedAudioSize(s_AudioDevice) - (int)s_BaselinePendingData, 0);
|
|
||||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * s_ChannelCount * sizeof(short));
|
|
||||||
|
|
||||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
|
|
||||||
if (framesQueued - s_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
|
||||||
s_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Pending hard drop of %u audio frames",
|
|
||||||
s_PendingHardDrops);
|
|
||||||
}
|
|
||||||
else if (framesQueued - s_PendingHardDrops - s_PendingDrops > MAX_QUEUED_FRAMES) {
|
|
||||||
s_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this frame should be dropped
|
|
||||||
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
|
||||||
s_PendingDrops = s_PendingHardDrops = 0;
|
|
||||||
}
|
|
||||||
else if (s_PendingHardDrops != 0) {
|
|
||||||
// Hard drops happen all at once to forcefully
|
|
||||||
// resync with the source.
|
|
||||||
s_PendingHardDrops--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (s_PendingDrops != 0 && s_SampleIndex % DROP_RATIO_DENOM == 0) {
|
|
||||||
// Normal drops are interspersed with the audio data
|
|
||||||
// to hide the glitches.
|
|
||||||
s_PendingDrops--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
samplesDecoded = opus_multistream_decode(s_OpusDecoder,
|
|
||||||
(unsigned char*)sampleData,
|
|
||||||
sampleLength,
|
|
||||||
s_OpusDecodeBuffer,
|
|
||||||
SAMPLES_PER_FRAME,
|
|
||||||
0);
|
|
||||||
if (samplesDecoded > 0) {
|
|
||||||
if (SDL_QueueAudio(s_AudioDevice,
|
|
||||||
s_OpusDecodeBuffer,
|
|
||||||
sizeof(short) * samplesDecoded * s_ChannelCount) < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to queue audio sample: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#include "../session.hpp"
|
||||||
|
#include "renderers/renderer.h"
|
||||||
|
#include "renderers/sdl.h"
|
||||||
|
#include "renderers/qtaud.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
IAudioRenderer* Session::createAudioRenderer()
|
||||||
|
{
|
||||||
|
return new SdlAudioRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
IAudioRenderer* audioRenderer;
|
||||||
|
|
||||||
|
audioRenderer = createAudioRenderer();
|
||||||
|
if (audioRenderer == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ret = audioRenderer->testAudio(audioConfiguration);
|
||||||
|
|
||||||
|
delete audioRenderer;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Session::detectAudioConfiguration()
|
||||||
|
{
|
||||||
|
IAudioRenderer* audioRenderer;
|
||||||
|
|
||||||
|
audioRenderer = createAudioRenderer();
|
||||||
|
if (audioRenderer == nullptr) {
|
||||||
|
// Hope for the best
|
||||||
|
return AUDIO_CONFIGURATION_STEREO;
|
||||||
|
}
|
||||||
|
|
||||||
|
int audioConfig = audioRenderer->detectAudioConfiguration();
|
||||||
|
|
||||||
|
delete audioRenderer;
|
||||||
|
|
||||||
|
return audioConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Session::arInit(int /* audioConfiguration */,
|
||||||
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
|
void* /* arContext */, int /* arFlags */)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
||||||
|
|
||||||
|
s_ActiveSession->m_OpusDecoder =
|
||||||
|
opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||||
|
opusConfig->channelCount,
|
||||||
|
opusConfig->streams,
|
||||||
|
opusConfig->coupledStreams,
|
||||||
|
opusConfig->mapping,
|
||||||
|
&error);
|
||||||
|
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create decoder: %d",
|
||||||
|
error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||||
|
if (s_ActiveSession->m_AudioRenderer == nullptr) {
|
||||||
|
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio stream has %d channels",
|
||||||
|
opusConfig->channelCount);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::arCleanup()
|
||||||
|
{
|
||||||
|
// m_AudioRenderer is deleted in cleanupAudioRenderer()
|
||||||
|
|
||||||
|
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||||
|
s_ActiveSession->m_OpusDecoder = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is called on the main thread
|
||||||
|
void Session::cleanupAudioRendererOnMainThread()
|
||||||
|
{
|
||||||
|
SDL_AtomicLock(&m_AudioRendererLock);
|
||||||
|
delete m_AudioRenderer;
|
||||||
|
m_AudioRenderer = nullptr;
|
||||||
|
SDL_AtomicUnlock(&m_AudioRendererLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||||
|
{
|
||||||
|
int samplesDecoded;
|
||||||
|
|
||||||
|
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
||||||
|
(unsigned char*)sampleData,
|
||||||
|
sampleLength,
|
||||||
|
s_ActiveSession->m_OpusDecodeBuffer,
|
||||||
|
SAMPLES_PER_FRAME,
|
||||||
|
0);
|
||||||
|
if (samplesDecoded > 0) {
|
||||||
|
SDL_AtomicLock(&s_ActiveSession->m_AudioRendererLock);
|
||||||
|
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||||
|
s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
|
||||||
|
static_cast<int>(sizeof(short) *
|
||||||
|
samplesDecoded *
|
||||||
|
s_ActiveSession->m_AudioConfig.channelCount));
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&s_ActiveSession->m_AudioRendererLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "qtaud.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
QtAudioRenderer::QtAudioRenderer()
|
||||||
|
: m_AudioOutput(nullptr),
|
||||||
|
m_OutputDevice(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QtAudioRenderer::~QtAudioRenderer()
|
||||||
|
{
|
||||||
|
delete m_AudioOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
QAudioFormat format;
|
||||||
|
|
||||||
|
// testAudio() assumes this sample rate
|
||||||
|
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||||
|
|
||||||
|
format.setSampleRate(opusConfig->sampleRate);
|
||||||
|
format.setChannelCount(opusConfig->channelCount);
|
||||||
|
format.setSampleSize(16);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
|
||||||
|
m_AudioOutput = new QAudioOutput(format);
|
||||||
|
|
||||||
|
// Buffer 40 ms of audio data
|
||||||
|
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||||
|
|
||||||
|
m_OutputDevice = m_AudioOutput->start();
|
||||||
|
if (m_OutputDevice == nullptr) {
|
||||||
|
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check what buffer size was really used after starting
|
||||||
|
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
QAudioFormat format;
|
||||||
|
|
||||||
|
format.setSampleRate(48000);
|
||||||
|
format.setSampleSize(16);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
|
||||||
|
switch (audioConfiguration) {
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
format.setChannelCount(2);
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
format.setChannelCount(6);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Q_ASSERT(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||||
|
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int QtAudioRenderer::detectAudioConfiguration()
|
||||||
|
{
|
||||||
|
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||||
|
|
||||||
|
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||||
|
|
||||||
|
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||||
|
if (preferredChannelCount > 2) {
|
||||||
|
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return AUDIO_CONFIGURATION_STEREO;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <QAudioOutput>
|
||||||
|
|
||||||
|
class QtAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QtAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~QtAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
virtual bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
virtual int detectAudioConfiguration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QAudioOutput* m_AudioOutput;
|
||||||
|
QIODevice* m_OutputDevice;
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
#define MAX_CHANNELS 6
|
||||||
|
#define SAMPLES_PER_FRAME 240
|
||||||
|
|
||||||
|
class IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IAudioRenderer() {}
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
||||||
|
|
||||||
|
virtual void submitAudio(short* audioBuffer, int audioSize) = 0;
|
||||||
|
|
||||||
|
virtual bool testAudio(int audioConfiguration) = 0;
|
||||||
|
|
||||||
|
virtual int detectAudioConfiguration() = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
class SdlAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SdlAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~SdlAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
virtual bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
virtual int detectAudioConfiguration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_AudioDeviceID m_AudioDevice;
|
||||||
|
Uint32 m_ChannelCount;
|
||||||
|
Uint32 m_PendingDrops;
|
||||||
|
Uint32 m_PendingHardDrops;
|
||||||
|
Uint32 m_SampleIndex;
|
||||||
|
Uint32 m_BaselinePendingData;
|
||||||
|
};
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#include "sdl.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QAudioDeviceInfo>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#define MIN_QUEUED_FRAMES 2
|
||||||
|
#define MAX_QUEUED_FRAMES 4
|
||||||
|
#define STOP_THE_WORLD_LIMIT 20
|
||||||
|
#define DROP_RATIO_DENOM 32
|
||||||
|
|
||||||
|
// Detecting this with SDL is quite problematic, so we'll use Qt's
|
||||||
|
// multimedia framework to do so. It appears to be actually
|
||||||
|
// accurate on Linux and macOS, unlike using SDL and relying
|
||||||
|
// on a channel change in the format received.
|
||||||
|
int SdlAudioRenderer::detectAudioConfiguration()
|
||||||
|
{
|
||||||
|
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio output device prefers %d channel configuration",
|
||||||
|
preferredChannelCount);
|
||||||
|
|
||||||
|
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||||
|
if (preferredChannelCount > 2) {
|
||||||
|
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return AUDIO_CONFIGURATION_STEREO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlAudioRenderer::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
SDL_AudioSpec want, have;
|
||||||
|
SDL_AudioDeviceID dev;
|
||||||
|
|
||||||
|
SDL_zero(want);
|
||||||
|
want.freq = 48000;
|
||||||
|
want.format = AUDIO_S16;
|
||||||
|
want.samples = SAMPLES_PER_FRAME;
|
||||||
|
|
||||||
|
switch (audioConfiguration) {
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
want.channels = 2;
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
want.channels = 6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test audio device for functionality
|
||||||
|
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||||
|
if (dev == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio test - Failed to open audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_CloseAudioDevice(dev);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio test - Successful with %d channels",
|
||||||
|
want.channels);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlAudioRenderer::SdlAudioRenderer()
|
||||||
|
: m_AudioDevice(0),
|
||||||
|
m_ChannelCount(0),
|
||||||
|
m_PendingDrops(0),
|
||||||
|
m_PendingHardDrops(0),
|
||||||
|
m_SampleIndex(0),
|
||||||
|
m_BaselinePendingData(0)
|
||||||
|
{
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_assert(SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
SDL_AudioSpec want, have;
|
||||||
|
|
||||||
|
SDL_zero(want);
|
||||||
|
want.freq = opusConfig->sampleRate;
|
||||||
|
want.format = AUDIO_S16;
|
||||||
|
want.channels = opusConfig->channelCount;
|
||||||
|
|
||||||
|
// This is supposed to be a power of 2, but our
|
||||||
|
// frames contain a non-power of 2 number of samples,
|
||||||
|
// so the slop would require buffering another full frame.
|
||||||
|
// Specifying non-Po2 seems to work for our supported platforms.
|
||||||
|
want.samples = SAMPLES_PER_FRAME;
|
||||||
|
|
||||||
|
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||||
|
if (m_AudioDevice == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Desired audio buffer: %u samples (%lu bytes)",
|
||||||
|
want.samples,
|
||||||
|
want.samples * sizeof(short) * want.channels);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Obtained audio buffer: %u samples (%u bytes)",
|
||||||
|
have.samples,
|
||||||
|
have.size);
|
||||||
|
|
||||||
|
// SDL counts pending samples in the queued
|
||||||
|
// audio size using the WASAPI backend. This
|
||||||
|
// includes silence, which can throw off our
|
||||||
|
// pending data count. Get a baseline so we
|
||||||
|
// can exclude that data.
|
||||||
|
m_BaselinePendingData = 0;
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
|
||||||
|
SDL_Delay(10);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
m_BaselinePendingData *= 2;
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Baseline pending audio data: %d bytes",
|
||||||
|
m_BaselinePendingData);
|
||||||
|
|
||||||
|
m_ChannelCount = opusConfig->channelCount;
|
||||||
|
m_SampleIndex = 0;
|
||||||
|
m_PendingDrops = m_PendingHardDrops = 0;
|
||||||
|
|
||||||
|
// Start playback
|
||||||
|
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlAudioRenderer::~SdlAudioRenderer()
|
||||||
|
{
|
||||||
|
if (m_AudioDevice != 0) {
|
||||||
|
// Stop playback
|
||||||
|
SDL_PauseAudioDevice(m_AudioDevice, 1);
|
||||||
|
SDL_CloseAudioDevice(m_AudioDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
m_SampleIndex++;
|
||||||
|
|
||||||
|
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
|
||||||
|
if (queuedAudio > m_BaselinePendingData) {
|
||||||
|
queuedAudio -= m_BaselinePendingData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queuedAudio = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
|
||||||
|
|
||||||
|
// We must check this prior to the below checks to ensure we don't
|
||||||
|
// underflow if framesQueued - m_PendingHardDrops < 0.
|
||||||
|
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
||||||
|
m_PendingDrops = m_PendingHardDrops = 0;
|
||||||
|
}
|
||||||
|
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
|
||||||
|
// to ensure we don't underflow.
|
||||||
|
else if (framesQueued > m_PendingHardDrops &&
|
||||||
|
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
||||||
|
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Pending hard drop of %u audio frames",
|
||||||
|
m_PendingHardDrops);
|
||||||
|
}
|
||||||
|
// If we're under the stop the world limit, we can drop samples
|
||||||
|
// gracefully over the next little while.
|
||||||
|
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
|
||||||
|
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
|
||||||
|
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this frame should be dropped
|
||||||
|
if (m_PendingHardDrops != 0) {
|
||||||
|
// Hard drops happen all at once to forcefully
|
||||||
|
// resync with the source.
|
||||||
|
m_PendingHardDrops--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
|
||||||
|
// Normal drops are interspersed with the audio data
|
||||||
|
// to hide the glitches.
|
||||||
|
m_PendingDrops--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to queue audio sample: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.hpp"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
#define VK_0 0x30
|
#define VK_0 0x30
|
||||||
#define VK_A 0x41
|
#define VK_A 0x41
|
||||||
@@ -9,7 +13,7 @@
|
|||||||
#ifndef VK_F1
|
#ifndef VK_F1
|
||||||
#define VK_F1 0x70
|
#define VK_F1 0x70
|
||||||
#define VK_F13 0x7C
|
#define VK_F13 0x7C
|
||||||
#define VK_NUMPAD1 0x61
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
@@ -20,13 +24,76 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||||
};
|
};
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer)
|
||||||
: m_MultiController(multiController)
|
: m_LastMouseMotionTime(0),
|
||||||
|
m_MultiController(prefs.multiController),
|
||||||
|
m_NeedsInputDelay(false)
|
||||||
{
|
{
|
||||||
|
// Allow gamepad input when the app doesn't have focus
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
|
||||||
|
// If mouse acceleration is enabled, use relative mode warp (which
|
||||||
|
// is via normal motion events that are influenced by mouse acceleration).
|
||||||
|
// Otherwise, we'll use raw input capture which is straight from the device
|
||||||
|
// without modification by the OS.
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||||
|
prefs.mouseAcceleration ? "1" : "0");
|
||||||
|
|
||||||
|
// We need to reinit this each time, since you only get
|
||||||
|
// an initial set of gamepad arrival events once per init.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString mappingFile = Path::getGamepadMappingFile();
|
||||||
|
if (!mappingFile.isEmpty()) {
|
||||||
|
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Loading gamepad mappings from: %s",
|
||||||
|
mappingFileNative.c_str());
|
||||||
|
|
||||||
|
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
||||||
|
if (newMappings < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Error loading gamepad mappings: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Loaded %d new gamepad mappings",
|
||||||
|
newMappings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No gamepad mapping file found");
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_MultiController) {
|
if (!m_MultiController) {
|
||||||
// Player 1 is always present in non-MC mode
|
// Player 1 is always present in non-MC mode
|
||||||
m_GamepadMask = 0x1;
|
m_GamepadMask = 0x1;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, detect gamepads on the fly
|
||||||
|
m_GamepadMask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prior to GFE 3.14.1, sending too many mouse motion events can cause
|
||||||
|
// GFE to choke and input latency to increase significantly. We will
|
||||||
|
// artificially throttle them to avoid this situation.
|
||||||
|
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
|
||||||
|
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||||
|
gfeVersion[0] < 3 ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"This older version of GFE requires input delay hack");
|
||||||
|
m_NeedsInputDelay = true;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
SDL_zero(m_GamepadState);
|
||||||
}
|
}
|
||||||
@@ -38,6 +105,9 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
@@ -51,26 +121,26 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
|
|
||||||
|
// Force raise all keys in the combo to avoid
|
||||||
|
// leaving them down after disconnecting
|
||||||
|
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
|
||||||
|
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
|
||||||
|
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
|
||||||
|
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
|
||||||
|
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
|
||||||
|
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
|
||||||
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
if (event->keysym.sym == SDLK_q) {
|
if (event->keysym.sym == SDLK_q) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected quit key combo");
|
"Detected quit key combo");
|
||||||
|
|
||||||
// Force raise all keys in the combo to avoid
|
// Uncapture the mouse to avoid processing any
|
||||||
// leaving them down after disconnecting
|
// further keyboard input
|
||||||
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
|
|
||||||
|
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
|
|
||||||
// Drain the event queue of any additional input
|
|
||||||
// that might be processed before our quit message.
|
|
||||||
while (SDL_PollEvent(&event));
|
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
// Push a quit event to the main loop
|
||||||
event.type = SDL_QUIT;
|
event.type = SDL_QUIT;
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
@@ -88,12 +158,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
else if (event->keysym.sym == SDLK_x) {
|
else if (event->keysym.sym == SDLK_x) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected full-screen toggle combo");
|
"Detected full-screen toggle combo");
|
||||||
if (SDL_GetWindowFlags(Session::s_ActiveSession->m_Window) & SDL_WINDOW_FULLSCREEN) {
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, 0);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_SetWindowFullscreen(Session::s_ActiveSession->m_Window, SDL_WINDOW_FULLSCREEN);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,195 +180,206 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
modifiers |= MODIFIER_SHIFT;
|
modifiers |= MODIFIER_SHIFT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set keycode
|
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
||||||
if (event->keysym.sym >= SDLK_0 && event->keysym.sym <= SDLK_9) {
|
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
||||||
keyCode = (event->keysym.sym - SDLK_0) + VK_0;
|
// a QWERTY layout to work.
|
||||||
|
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
|
||||||
|
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_a && event->keysym.sym <= SDLK_z) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
|
||||||
keyCode = (event->keysym.sym - SDLK_a) + VK_A;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_F1 && event->keysym.sym <= SDLK_F12) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
|
||||||
keyCode = (event->keysym.sym - SDLK_F1) + VK_F1;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_F13 && event->keysym.sym <= SDLK_F24) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
|
||||||
keyCode = (event->keysym.sym - SDLK_F13) + VK_F13;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
|
||||||
}
|
}
|
||||||
else if (event->keysym.sym >= SDLK_KP_1 && event->keysym.sym <= SDLK_KP_9) {
|
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
|
||||||
// SDL defines SDLK_KP_0 > SDLK_KP_9, so we need to handle that manually
|
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
|
||||||
keyCode = (event->keysym.sym - SDLK_KP_1) + VK_NUMPAD1;
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
switch (event->keysym.sym) {
|
switch (event->keysym.scancode) {
|
||||||
case SDLK_BACKSPACE:
|
case SDL_SCANCODE_BACKSPACE:
|
||||||
keyCode = 0x08;
|
keyCode = 0x08;
|
||||||
break;
|
break;
|
||||||
case SDLK_TAB:
|
case SDL_SCANCODE_TAB:
|
||||||
keyCode = 0x09;
|
keyCode = 0x09;
|
||||||
break;
|
break;
|
||||||
case SDLK_CLEAR:
|
case SDL_SCANCODE_CLEAR:
|
||||||
keyCode = 0x0C;
|
keyCode = 0x0C;
|
||||||
break;
|
break;
|
||||||
case SDLK_RETURN:
|
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
|
||||||
|
case SDL_SCANCODE_RETURN:
|
||||||
keyCode = 0x0D;
|
keyCode = 0x0D;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAUSE:
|
case SDL_SCANCODE_PAUSE:
|
||||||
keyCode = 0x13;
|
keyCode = 0x13;
|
||||||
break;
|
break;
|
||||||
case SDLK_CAPSLOCK:
|
case SDL_SCANCODE_CAPSLOCK:
|
||||||
keyCode = 0x14;
|
keyCode = 0x14;
|
||||||
break;
|
break;
|
||||||
case SDLK_ESCAPE:
|
case SDL_SCANCODE_ESCAPE:
|
||||||
keyCode = 0x1B;
|
keyCode = 0x1B;
|
||||||
break;
|
break;
|
||||||
case SDLK_SPACE:
|
case SDL_SCANCODE_SPACE:
|
||||||
keyCode = 0x20;
|
keyCode = 0x20;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAGEUP:
|
case SDL_SCANCODE_PAGEUP:
|
||||||
keyCode = 0x21;
|
keyCode = 0x21;
|
||||||
break;
|
break;
|
||||||
case SDLK_PAGEDOWN:
|
case SDL_SCANCODE_PAGEDOWN:
|
||||||
keyCode = 0x22;
|
keyCode = 0x22;
|
||||||
break;
|
break;
|
||||||
case SDLK_END:
|
case SDL_SCANCODE_END:
|
||||||
keyCode = 0x23;
|
keyCode = 0x23;
|
||||||
break;
|
break;
|
||||||
case SDLK_HOME:
|
case SDL_SCANCODE_HOME:
|
||||||
keyCode = 0x24;
|
keyCode = 0x24;
|
||||||
break;
|
break;
|
||||||
case SDLK_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
keyCode = 0x25;
|
keyCode = 0x25;
|
||||||
break;
|
break;
|
||||||
case SDLK_UP:
|
case SDL_SCANCODE_UP:
|
||||||
keyCode = 0x26;
|
keyCode = 0x26;
|
||||||
break;
|
break;
|
||||||
case SDLK_RIGHT:
|
case SDL_SCANCODE_RIGHT:
|
||||||
keyCode = 0x27;
|
keyCode = 0x27;
|
||||||
break;
|
break;
|
||||||
case SDLK_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
keyCode = 0x28;
|
keyCode = 0x28;
|
||||||
break;
|
break;
|
||||||
case SDLK_SELECT:
|
case SDL_SCANCODE_SELECT:
|
||||||
keyCode = 0x29;
|
keyCode = 0x29;
|
||||||
break;
|
break;
|
||||||
case SDLK_EXECUTE:
|
case SDL_SCANCODE_EXECUTE:
|
||||||
keyCode = 0x2B;
|
keyCode = 0x2B;
|
||||||
break;
|
break;
|
||||||
case SDLK_PRINTSCREEN:
|
case SDL_SCANCODE_PRINTSCREEN:
|
||||||
keyCode = 0x2C;
|
keyCode = 0x2C;
|
||||||
break;
|
break;
|
||||||
case SDLK_INSERT:
|
case SDL_SCANCODE_INSERT:
|
||||||
keyCode = 0x2D;
|
keyCode = 0x2D;
|
||||||
break;
|
break;
|
||||||
case SDLK_DELETE:
|
case SDL_SCANCODE_DELETE:
|
||||||
keyCode = 0x2E;
|
keyCode = 0x2E;
|
||||||
break;
|
break;
|
||||||
case SDLK_HELP:
|
case SDL_SCANCODE_HELP:
|
||||||
keyCode = 0x2F;
|
keyCode = 0x2F;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_0:
|
case SDL_SCANCODE_KP_0:
|
||||||
// See comment above about why we only handle KP_0 here
|
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
|
||||||
keyCode = 0x60;
|
keyCode = VK_NUMPAD0;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_MULTIPLY:
|
case SDL_SCANCODE_0:
|
||||||
|
// See comment above about why we only handle SDL_SCANCODE_0 here
|
||||||
|
keyCode = VK_0;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_MULTIPLY:
|
||||||
keyCode = 0x6A;
|
keyCode = 0x6A;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_PLUS:
|
case SDL_SCANCODE_KP_PLUS:
|
||||||
keyCode = 0x6B;
|
keyCode = 0x6B;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_COMMA:
|
case SDL_SCANCODE_KP_COMMA:
|
||||||
keyCode = 0x6C;
|
keyCode = 0x6C;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_MINUS:
|
case SDL_SCANCODE_KP_MINUS:
|
||||||
keyCode = 0x6D;
|
keyCode = 0x6D;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_DECIMAL:
|
case SDL_SCANCODE_KP_PERIOD:
|
||||||
keyCode = 0x6E;
|
keyCode = 0x6E;
|
||||||
break;
|
break;
|
||||||
case SDLK_KP_DIVIDE:
|
case SDL_SCANCODE_KP_DIVIDE:
|
||||||
keyCode = 0x6F;
|
keyCode = 0x6F;
|
||||||
break;
|
break;
|
||||||
case SDLK_NUMLOCKCLEAR:
|
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||||
keyCode = 0x90;
|
keyCode = 0x90;
|
||||||
break;
|
break;
|
||||||
case SDLK_SCROLLLOCK:
|
case SDL_SCANCODE_SCROLLLOCK:
|
||||||
keyCode = 0x91;
|
keyCode = 0x91;
|
||||||
break;
|
break;
|
||||||
case SDLK_LSHIFT:
|
case SDL_SCANCODE_LSHIFT:
|
||||||
keyCode = 0xA0;
|
keyCode = 0xA0;
|
||||||
break;
|
break;
|
||||||
case SDLK_RSHIFT:
|
case SDL_SCANCODE_RSHIFT:
|
||||||
keyCode = 0xA1;
|
keyCode = 0xA1;
|
||||||
break;
|
break;
|
||||||
case SDLK_LCTRL:
|
case SDL_SCANCODE_LCTRL:
|
||||||
keyCode = 0xA2;
|
keyCode = 0xA2;
|
||||||
break;
|
break;
|
||||||
case SDLK_RCTRL:
|
case SDL_SCANCODE_RCTRL:
|
||||||
keyCode = 0xA3;
|
keyCode = 0xA3;
|
||||||
break;
|
break;
|
||||||
case SDLK_LALT:
|
case SDL_SCANCODE_LALT:
|
||||||
keyCode = 0xA4;
|
keyCode = 0xA4;
|
||||||
break;
|
break;
|
||||||
case SDLK_RALT:
|
case SDL_SCANCODE_RALT:
|
||||||
keyCode = 0xA5;
|
keyCode = 0xA5;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_BACK:
|
case SDL_SCANCODE_AC_BACK:
|
||||||
keyCode = 0xA6;
|
keyCode = 0xA6;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_FORWARD:
|
case SDL_SCANCODE_AC_FORWARD:
|
||||||
keyCode = 0xA7;
|
keyCode = 0xA7;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_REFRESH:
|
case SDL_SCANCODE_AC_REFRESH:
|
||||||
keyCode = 0xA8;
|
keyCode = 0xA8;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_STOP:
|
case SDL_SCANCODE_AC_STOP:
|
||||||
keyCode = 0xA9;
|
keyCode = 0xA9;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_SEARCH:
|
case SDL_SCANCODE_AC_SEARCH:
|
||||||
keyCode = 0xAA;
|
keyCode = 0xAA;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_BOOKMARKS:
|
case SDL_SCANCODE_AC_BOOKMARKS:
|
||||||
keyCode = 0xAB;
|
keyCode = 0xAB;
|
||||||
break;
|
break;
|
||||||
case SDLK_AC_HOME:
|
case SDL_SCANCODE_AC_HOME:
|
||||||
keyCode = 0xAC;
|
keyCode = 0xAC;
|
||||||
break;
|
break;
|
||||||
case SDLK_SEMICOLON:
|
case SDL_SCANCODE_SEMICOLON:
|
||||||
keyCode = 0xBA;
|
keyCode = 0xBA;
|
||||||
break;
|
break;
|
||||||
case SDLK_PLUS:
|
case SDL_SCANCODE_EQUALS:
|
||||||
keyCode = 0xBB;
|
keyCode = 0xBB;
|
||||||
break;
|
break;
|
||||||
case SDLK_COMMA:
|
case SDL_SCANCODE_COMMA:
|
||||||
keyCode = 0xBC;
|
keyCode = 0xBC;
|
||||||
break;
|
break;
|
||||||
case SDLK_MINUS:
|
case SDL_SCANCODE_MINUS:
|
||||||
keyCode = 0xBD;
|
keyCode = 0xBD;
|
||||||
break;
|
break;
|
||||||
case SDLK_PERIOD:
|
case SDL_SCANCODE_PERIOD:
|
||||||
keyCode = 0xBE;
|
keyCode = 0xBE;
|
||||||
break;
|
break;
|
||||||
case SDLK_SLASH:
|
case SDL_SCANCODE_SLASH:
|
||||||
keyCode = 0xBF;
|
keyCode = 0xBF;
|
||||||
break;
|
break;
|
||||||
case SDLK_BACKQUOTE:
|
case SDL_SCANCODE_GRAVE:
|
||||||
keyCode = 0xC0;
|
keyCode = 0xC0;
|
||||||
break;
|
break;
|
||||||
case SDLK_LEFTBRACKET:
|
case SDL_SCANCODE_LEFTBRACKET:
|
||||||
keyCode = 0xDB;
|
keyCode = 0xDB;
|
||||||
break;
|
break;
|
||||||
case SDLK_BACKSLASH:
|
case SDL_SCANCODE_BACKSLASH:
|
||||||
keyCode = 0xDC;
|
keyCode = 0xDC;
|
||||||
break;
|
break;
|
||||||
case SDLK_RIGHTBRACKET:
|
case SDL_SCANCODE_RIGHTBRACKET:
|
||||||
keyCode = 0xDD;
|
keyCode = 0xDD;
|
||||||
break;
|
break;
|
||||||
case SDLK_QUOTE:
|
case SDL_SCANCODE_APOSTROPHE:
|
||||||
keyCode = 0xDE;
|
keyCode = 0xDE;
|
||||||
break;
|
break;
|
||||||
|
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||||
|
keyCode = 0xE2;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unhandled button event: %d",
|
"Unhandled button event: %d",
|
||||||
event->keysym.sym);
|
event->keysym.scancode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,9 +394,13 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
{
|
{
|
||||||
int button;
|
int button;
|
||||||
|
|
||||||
// Capture the mouse again if clicked when unbound
|
// Capture the mouse again if clicked when unbound.
|
||||||
|
// We start capture on left button released instead of
|
||||||
|
// pressed to avoid sending an errant mouse button released
|
||||||
|
// event to the host when clicking into our window (since
|
||||||
|
// the pressed event was consumed by this code).
|
||||||
if (event->button == SDL_BUTTON_LEFT &&
|
if (event->button == SDL_BUTTON_LEFT &&
|
||||||
event->state == SDL_PRESSED &&
|
event->state == SDL_RELEASED &&
|
||||||
!SDL_GetRelativeMouseMode()) {
|
!SDL_GetRelativeMouseMode()) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
return;
|
return;
|
||||||
@@ -364,10 +444,39 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
short xdelta = (short)event->xrel;
|
||||||
|
short ydelta = (short)event->yrel;
|
||||||
|
|
||||||
if (event->xrel != 0 || event->yrel != 0) {
|
// If we're sending more than one motion event per millisecond,
|
||||||
LiSendMouseMoveEvent((unsigned short)event->xrel,
|
// delay for 1 ms to allow batching of mouse move events. On older
|
||||||
(unsigned short)event->yrel);
|
// versions of GFE, we will unconditionally wait this 1 ms to
|
||||||
|
// work around an input processing issue that causes massive mouse latency.
|
||||||
|
Uint32 currentTime = SDL_GetTicks();
|
||||||
|
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
currentTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
m_LastMouseMotionTime = currentTime;
|
||||||
|
|
||||||
|
// Pump even if we didn't delay since we might get some extra events
|
||||||
|
SDL_PumpEvents();
|
||||||
|
|
||||||
|
// Batch all of the pending mouse motion events
|
||||||
|
SDL_Event nextEvent;
|
||||||
|
while (SDL_PeepEvents(&nextEvent,
|
||||||
|
1,
|
||||||
|
SDL_GETEVENT,
|
||||||
|
SDL_MOUSEMOTION,
|
||||||
|
SDL_MOUSEMOTION) == 1) {
|
||||||
|
// In theory, these can overflow but in practice
|
||||||
|
// it should be highly unlikely since it would require
|
||||||
|
// moving 64K pixels in 1 ms.
|
||||||
|
xdelta += nextEvent.motion.xrel;
|
||||||
|
ydelta += nextEvent.motion.yrel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xdelta != 0 || ydelta != 0) {
|
||||||
|
LiSendMouseMoveEvent(xdelta, ydelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,13 +536,18 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
|||||||
state->lsX = event->value;
|
state->lsX = event->value;
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_LEFTY:
|
case SDL_CONTROLLER_AXIS_LEFTY:
|
||||||
state->lsY = -event->value;
|
// Signed values have one more negative value than
|
||||||
|
// positive value, so inverting the sign on -32768
|
||||||
|
// could actually cause the value to overflow and
|
||||||
|
// wrap around to be negative again. Avoid that by
|
||||||
|
// capping the value at 32767.
|
||||||
|
state->lsY = -qMax(event->value, (short)-32767);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTX:
|
case SDL_CONTROLLER_AXIS_RIGHTX:
|
||||||
state->rsX = event->value;
|
state->rsX = event->value;
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTY:
|
case SDL_CONTROLLER_AXIS_RIGHTY:
|
||||||
state->rsY = -event->value;
|
state->rsY = -qMax(event->value, (short)-32767);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
||||||
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
struct GamepadState {
|
struct GamepadState {
|
||||||
@@ -18,7 +21,7 @@ struct GamepadState {
|
|||||||
class SdlInputHandler
|
class SdlInputHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit SdlInputHandler(bool multiController);
|
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer);
|
||||||
|
|
||||||
~SdlInputHandler();
|
~SdlInputHandler();
|
||||||
|
|
||||||
@@ -44,7 +47,9 @@ private:
|
|||||||
|
|
||||||
void sendGamepadState(GamepadState* state);
|
void sendGamepadState(GamepadState* state);
|
||||||
|
|
||||||
|
Uint32 m_LastMouseMotionTime;
|
||||||
bool m_MultiController;
|
bool m_MultiController;
|
||||||
|
bool m_NeedsInputDelay;
|
||||||
int m_GamepadMask;
|
int m_GamepadMask;
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "input.hpp"
|
#include "input.hpp"
|
||||||
#include "video/decoder.h"
|
#include "video/decoder.h"
|
||||||
|
#include "audio/renderers/renderer.h"
|
||||||
|
|
||||||
class Session : public QObject
|
class Session : public QObject
|
||||||
{
|
{
|
||||||
@@ -19,7 +20,15 @@ class Session : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app);
|
explicit Session(NvComputer* computer, NvApp& app);
|
||||||
|
|
||||||
Q_INVOKABLE void exec();
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
|
static
|
||||||
|
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
@@ -33,20 +42,33 @@ signals:
|
|||||||
void displayLaunchWarning(QString text);
|
void displayLaunchWarning(QString text);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void initialize();
|
||||||
|
|
||||||
bool validateLaunch();
|
bool validateLaunch();
|
||||||
|
|
||||||
|
void emitLaunchWarning(QString text);
|
||||||
|
|
||||||
int getDecoderCapabilities();
|
int getDecoderCapabilities();
|
||||||
|
|
||||||
int sdlDetermineAudioConfiguration();
|
IAudioRenderer* createAudioRenderer();
|
||||||
|
|
||||||
static
|
int detectAudioConfiguration();
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
|
||||||
int videoFormat, int width, int height, int frameRate);
|
void cleanupAudioRendererOnMainThread();
|
||||||
|
|
||||||
|
bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
void getWindowDimensions(int& x, int& y,
|
||||||
|
int& width, int& height);
|
||||||
|
|
||||||
|
void toggleFullscreen();
|
||||||
|
|
||||||
|
void updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, IVideoDecoder*& chosenDecoder);
|
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
|
||||||
|
|
||||||
static
|
static
|
||||||
void clStageStarting(int stage);
|
void clStageStarting(int stage);
|
||||||
@@ -60,6 +82,17 @@ private:
|
|||||||
static
|
static
|
||||||
void clLogMessage(const char* format, ...);
|
void clLogMessage(const char* format, ...);
|
||||||
|
|
||||||
|
static
|
||||||
|
int arInit(int audioConfiguration,
|
||||||
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
|
void* arContext, int arFlags);
|
||||||
|
|
||||||
|
static
|
||||||
|
void arCleanup();
|
||||||
|
|
||||||
|
static
|
||||||
|
void arDecodeAndPlaySample(char* sampleData, int sampleLength);
|
||||||
|
|
||||||
static
|
static
|
||||||
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
|
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
|
||||||
|
|
||||||
@@ -69,23 +102,6 @@ private:
|
|||||||
static
|
static
|
||||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
static
|
|
||||||
int sdlAudioInit(int audioConfiguration,
|
|
||||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
|
||||||
void* arContext, int arFlags);
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioStart();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioStop();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioCleanup();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength);
|
|
||||||
|
|
||||||
StreamingPreferences m_Preferences;
|
StreamingPreferences m_Preferences;
|
||||||
STREAM_CONFIGURATION m_StreamConfig;
|
STREAM_CONFIGURATION m_StreamConfig;
|
||||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||||
@@ -94,15 +110,23 @@ private:
|
|||||||
SDL_Window* m_Window;
|
SDL_Window* m_Window;
|
||||||
IVideoDecoder* m_VideoDecoder;
|
IVideoDecoder* m_VideoDecoder;
|
||||||
SDL_SpinLock m_DecoderLock;
|
SDL_SpinLock m_DecoderLock;
|
||||||
|
bool m_NeedsIdr;
|
||||||
|
bool m_AudioDisabled;
|
||||||
|
Uint32 m_FullScreenFlag;
|
||||||
|
int m_DisplayOriginX;
|
||||||
|
int m_DisplayOriginY;
|
||||||
|
bool m_PendingWindowedTransition;
|
||||||
|
|
||||||
static SDL_AudioDeviceID s_AudioDevice;
|
int m_ActiveVideoFormat;
|
||||||
static OpusMSDecoder* s_OpusDecoder;
|
int m_ActiveVideoWidth;
|
||||||
static short s_OpusDecodeBuffer[];
|
int m_ActiveVideoHeight;
|
||||||
static int s_ChannelCount;
|
int m_ActiveVideoFrameRate;
|
||||||
static int s_PendingDrops;
|
|
||||||
static int s_PendingHardDrops;
|
OpusMSDecoder* m_OpusDecoder;
|
||||||
static unsigned int s_SampleIndex;
|
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
||||||
static Uint32 s_BaselinePendingData;
|
IAudioRenderer* m_AudioRenderer;
|
||||||
|
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
||||||
|
SDL_SpinLock m_AudioRendererLock;
|
||||||
|
|
||||||
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
||||||
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#include "streamutils.h"
|
||||||
|
|
||||||
|
#include <Qt>
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||||
|
{
|
||||||
|
int dstH = dst->w * src->h / src->w;
|
||||||
|
int dstW = dst->h * src->w / src->h;
|
||||||
|
|
||||||
|
if (dstH > dst->h) {
|
||||||
|
dst->y = 0;
|
||||||
|
dst->x = (dst->w - dstW) / 2;
|
||||||
|
dst->w = dstW;
|
||||||
|
SDL_assert(dst->w * src->h / src->w <= dst->h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dst->x = 0;
|
||||||
|
dst->y = (dst->h - dstH) / 2;
|
||||||
|
dst->h = dstH;
|
||||||
|
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
|
||||||
|
{
|
||||||
|
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
||||||
|
if (displayIndex < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to get current display: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume display 0 if it fails
|
||||||
|
displayIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
// Use the window display mode for full-screen exclusive mode
|
||||||
|
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the current display mode for windowed and borderless
|
||||||
|
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be zero if undefined
|
||||||
|
if (mode.refresh_rate == 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Refresh rate unknown; assuming 60 Hz");
|
||||||
|
mode.refresh_rate = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode.refresh_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#define MAX_DISPLAYS 16
|
||||||
|
CGDirectDisplayID displayIds[MAX_DISPLAYS];
|
||||||
|
uint32_t displayCount = 0;
|
||||||
|
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
|
||||||
|
if (displayIndex >= displayCount) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Too many displays: %d vs %d",
|
||||||
|
displayIndex, displayCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_zerop(mode);
|
||||||
|
|
||||||
|
// Retina displays have non-native resolutions both below and above (!) their
|
||||||
|
// native resolution, so it's impossible for us to figure out what's actually
|
||||||
|
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
|
||||||
|
// find the correct resolution and match it in our SDL list.
|
||||||
|
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
|
||||||
|
CFIndex count = CFArrayGetCount(modeList);
|
||||||
|
for (CFIndex i = 0; i < count; i++) {
|
||||||
|
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
|
||||||
|
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
|
||||||
|
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
|
||||||
|
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFRelease(modeList);
|
||||||
|
|
||||||
|
// Now find the SDL mode that matches the CG native mode
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
SDL_DisplayMode thisMode;
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
|
||||||
|
if (thisMode.w == mode->w && thisMode.h == mode->h &&
|
||||||
|
thisMode.refresh_rate >= mode->refresh_rate) {
|
||||||
|
*mode = thisMode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
class StreamUtils
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static
|
||||||
|
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||||
|
|
||||||
|
static
|
||||||
|
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDisplayRefreshRate(SDL_Window* window);
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "decoder.h"
|
|
||||||
|
|
||||||
// A factory is required to avoid pulling in
|
|
||||||
// incompatible Objective-C headers.
|
|
||||||
class AVFoundationDecoderFactory {
|
|
||||||
public:
|
|
||||||
static
|
|
||||||
IVideoDecoder* createDecoder();
|
|
||||||
};
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
#include "avfoundation.h"
|
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
|
||||||
#import <CoreVideo/CoreVideo.h>
|
|
||||||
|
|
||||||
#define FRAME_START_PREFIX_SIZE 4
|
|
||||||
#define NALU_START_PREFIX_SIZE 3
|
|
||||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
|
||||||
|
|
||||||
#define FUTURE_FRAMES_IN_VSYNC 3
|
|
||||||
|
|
||||||
class AVFoundationDecoder : public IVideoDecoder
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
AVFoundationDecoder()
|
|
||||||
: m_DisplayLayer(nullptr),
|
|
||||||
m_FormatDesc(nullptr),
|
|
||||||
m_SpsData(nullptr),
|
|
||||||
m_PpsData(nullptr),
|
|
||||||
m_VpsData(nullptr),
|
|
||||||
m_View(nullptr),
|
|
||||||
m_NeedsIdr(false),
|
|
||||||
m_DisplayLink(nullptr),
|
|
||||||
m_VsyncTime(0),
|
|
||||||
m_LastVsyncTime(0),
|
|
||||||
m_VsyncStatsLock(0)
|
|
||||||
{
|
|
||||||
SDL_zero(m_FrameCountOnVsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual ~AVFoundationDecoder()
|
|
||||||
{
|
|
||||||
if (m_FormatDesc != nullptr) {
|
|
||||||
CFRelease(m_FormatDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_DisplayLink != nullptr) {
|
|
||||||
CVDisplayLinkStop(m_DisplayLink);
|
|
||||||
CVDisplayLinkRelease(m_DisplayLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete m_SpsData;
|
|
||||||
delete m_PpsData;
|
|
||||||
delete m_VpsData;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
CVReturn
|
|
||||||
displayLinkOutputCallback(
|
|
||||||
CVDisplayLinkRef,
|
|
||||||
const CVTimeStamp*,
|
|
||||||
const CVTimeStamp* vsyncTime,
|
|
||||||
CVOptionFlags,
|
|
||||||
CVOptionFlags*,
|
|
||||||
void *displayLinkContext)
|
|
||||||
{
|
|
||||||
AVFoundationDecoder* me = reinterpret_cast<AVFoundationDecoder*>(displayLinkContext);
|
|
||||||
|
|
||||||
SDL_AtomicLock(&me->m_VsyncStatsLock);
|
|
||||||
me->m_LastVsyncTime = me->m_VsyncTime;
|
|
||||||
me->m_VsyncTime = vsyncTime->hostTime;
|
|
||||||
|
|
||||||
int framesThisVsync = *(volatile int*)&me->m_FrameCountOnVsync[0];
|
|
||||||
|
|
||||||
memmove(&me->m_FrameCountOnVsync[0],
|
|
||||||
&me->m_FrameCountOnVsync[1],
|
|
||||||
sizeof(me->m_FrameCountOnVsync) - sizeof(me->m_FrameCountOnVsync[0]));
|
|
||||||
me->m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC - 1] = 0;
|
|
||||||
SDL_AtomicUnlock(&me->m_VsyncStatsLock);
|
|
||||||
|
|
||||||
if (framesThisVsync != 1)
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"V-Sync interval frames: %d",
|
|
||||||
framesThisVsync);
|
|
||||||
|
|
||||||
return kCVReturnSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int, int, int) override
|
|
||||||
{
|
|
||||||
m_VideoFormat = videoFormat;
|
|
||||||
|
|
||||||
// Fail if the user wants software decode only
|
|
||||||
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
// Prior to 10.13, we'll just assume everything has
|
|
||||||
// H.264 support and fail open to allow VT decode.
|
|
||||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
|
|
||||||
if (__builtin_available(macOS 10.13, *)) {
|
|
||||||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_H264)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"No HW accelerated H.264 decode via VT");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Assuming H.264 HW decode on < macOS 10.13");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
|
||||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
|
|
||||||
if (__builtin_available(macOS 10.13, *)) {
|
|
||||||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"No HW accelerated HEVC decode via VT");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
// Fail closed for HEVC if we're not on 10.13+
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"No HEVC support on < macOS 10.13");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_SysWMinfo info;
|
|
||||||
|
|
||||||
SDL_VERSION(&info.version);
|
|
||||||
|
|
||||||
if (!SDL_GetWindowWMInfo(window, &info)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_GetWindowWMInfo() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
|
|
||||||
|
|
||||||
// SDL adds its own content view to listen for events.
|
|
||||||
// We need to add a subview for our display layer.
|
|
||||||
NSView* contentView = info.info.cocoa.window.contentView;
|
|
||||||
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
|
|
||||||
m_View.wantsLayer = YES;
|
|
||||||
[contentView addSubview: m_View];
|
|
||||||
|
|
||||||
setupDisplayLayer();
|
|
||||||
|
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
|
||||||
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, (void*)this);
|
|
||||||
CVDisplayLinkStart(m_DisplayLink);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual bool isHardwareAccelerated() override
|
|
||||||
{
|
|
||||||
// We only use AVFoundation for HW acceleration
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override
|
|
||||||
{
|
|
||||||
OSStatus status;
|
|
||||||
|
|
||||||
unsigned char* data = (unsigned char*)malloc(du->fullLength);
|
|
||||||
if (!data) {
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
PLENTRY entry = du->bufferList;
|
|
||||||
int pictureDataLength = 0;
|
|
||||||
while (entry != nullptr) {
|
|
||||||
if (entry->bufferType == BUFFER_TYPE_VPS) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got VPS");
|
|
||||||
delete m_VpsData;
|
|
||||||
m_VpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
|
|
||||||
|
|
||||||
// We got a new VPS so wait for a new SPS to match it
|
|
||||||
delete m_SpsData;
|
|
||||||
delete m_PpsData;
|
|
||||||
m_SpsData = m_PpsData = nullptr;
|
|
||||||
}
|
|
||||||
else if (entry->bufferType == BUFFER_TYPE_SPS) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got SPS");
|
|
||||||
delete m_SpsData;
|
|
||||||
m_SpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
|
|
||||||
|
|
||||||
// We got a new SPS so wait for a new PPS to match it
|
|
||||||
delete m_PpsData;
|
|
||||||
m_PpsData = nullptr;
|
|
||||||
}
|
|
||||||
else if (entry->bufferType == BUFFER_TYPE_PPS) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Got PPS");
|
|
||||||
delete m_PpsData;
|
|
||||||
m_PpsData = new QByteArray(&entry->data[FRAME_START_PREFIX_SIZE], entry->length - FRAME_START_PREFIX_SIZE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Picture data
|
|
||||||
SDL_assert(entry->bufferType == BUFFER_TYPE_PICDATA);
|
|
||||||
memcpy(&data[pictureDataLength],
|
|
||||||
entry->data,
|
|
||||||
entry->length);
|
|
||||||
pictureDataLength += entry->length;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = entry->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_assert(pictureDataLength <= du->fullLength);
|
|
||||||
|
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
|
||||||
m_NeedsIdr = false;
|
|
||||||
|
|
||||||
if (m_FormatDesc != nullptr) {
|
|
||||||
CFRelease(m_FormatDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_VideoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_SpsData->data(),
|
|
||||||
(uint8_t*)m_PpsData->data() };
|
|
||||||
const size_t parameterSetSizes[] = { (size_t)m_SpsData->length(),
|
|
||||||
(size_t)m_PpsData->length() };
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new H264 format description");
|
|
||||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
|
||||||
2, /* count of parameter sets */
|
|
||||||
parameterSetPointers,
|
|
||||||
parameterSetSizes,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
&m_FormatDesc);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create H264 format description: %d",
|
|
||||||
(int)status);
|
|
||||||
m_FormatDesc = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300
|
|
||||||
const uint8_t* const parameterSetPointers[] = { (uint8_t*)m_VpsData->data(),
|
|
||||||
(uint8_t*)m_SpsData->data(),
|
|
||||||
(uint8_t*)m_PpsData->data() };
|
|
||||||
const size_t parameterSetSizes[] = { (size_t)m_VpsData->length(),
|
|
||||||
(size_t)m_SpsData->length(),
|
|
||||||
(size_t)m_PpsData->length() };
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Constructing new HEVC format description");
|
|
||||||
|
|
||||||
if (__builtin_available(macOS 10.13, *)) {
|
|
||||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
|
||||||
3, /* count of parameter sets */
|
|
||||||
parameterSetPointers,
|
|
||||||
parameterSetSizes,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
nil,
|
|
||||||
&m_FormatDesc);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create HEVC format description: %d",
|
|
||||||
(int)status);
|
|
||||||
m_FormatDesc = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
// This means Moonlight-common-c decided to give us an HEVC stream
|
|
||||||
// even though we said we couldn't support it. All we can do is abort().
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_FormatDesc == nullptr || m_NeedsIdr) {
|
|
||||||
// Can't decode if we haven't gotten our parameter sets yet
|
|
||||||
free(data);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we're decoding actual frame data here
|
|
||||||
CMBlockBufferRef blockBuffer;
|
|
||||||
|
|
||||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferCreateEmpty failed: %d",
|
|
||||||
(int)status);
|
|
||||||
free(data);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
int lastOffset = -1;
|
|
||||||
for (int i = 0; i < pictureDataLength - FRAME_START_PREFIX_SIZE; i++) {
|
|
||||||
// Search for a NALU
|
|
||||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
|
||||||
// It's the start of a new NALU
|
|
||||||
if (lastOffset != -1) {
|
|
||||||
// We've seen a start before this so enqueue that NALU
|
|
||||||
updateBufferForRange(blockBuffer, data, lastOffset, i - lastOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOffset = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastOffset != -1) {
|
|
||||||
// Enqueue the remaining data
|
|
||||||
updateBufferForRange(blockBuffer, data, lastOffset, pictureDataLength - lastOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// From now on, CMBlockBuffer owns the data pointer and will free it when it's released
|
|
||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
|
||||||
|
|
||||||
CMSampleTimingInfo time = {
|
|
||||||
.decodeTimeStamp = kCMTimeInvalid,
|
|
||||||
.duration = kCMTimeInvalid
|
|
||||||
};
|
|
||||||
|
|
||||||
SDL_AtomicLock(&m_VsyncStatsLock);
|
|
||||||
// Find a slot to put this frame
|
|
||||||
for (int i = 0; i < FUTURE_FRAMES_IN_VSYNC; i++) {
|
|
||||||
if (m_FrameCountOnVsync[i] == 0) {
|
|
||||||
time.presentationTimeStamp =
|
|
||||||
CMTimeMake(m_VsyncTime + (i + 1) * (m_VsyncTime - m_LastVsyncTime),
|
|
||||||
1000 * 1000 * 1000);
|
|
||||||
m_FrameCountOnVsync[i]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SDL_AtomicUnlock(&m_VsyncStatsLock);
|
|
||||||
|
|
||||||
status = CMSampleBufferCreate(kCFAllocatorDefault,
|
|
||||||
blockBuffer,
|
|
||||||
true, NULL,
|
|
||||||
NULL, m_FormatDesc, 1, 1,
|
|
||||||
&time, 0, NULL,
|
|
||||||
&sampleBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMSampleBufferCreate failed: %d",
|
|
||||||
(int)status);
|
|
||||||
CFRelease(blockBuffer);
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit for callback on the main thread
|
|
||||||
queueFrame(sampleBuffer, (void*)(long)du->frameType);
|
|
||||||
|
|
||||||
return DR_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is called on the main thread, so it's safe to interact with
|
|
||||||
// the AVSampleBufferDisplayLayer here.
|
|
||||||
void submitBuffer(CMSampleBufferRef sampleBuffer, int frameType)
|
|
||||||
{
|
|
||||||
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Resetting failed AVSampleBufferDisplay layer");
|
|
||||||
setupDisplayLayer();
|
|
||||||
CFRelease(sampleBuffer);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
|
|
||||||
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
|
|
||||||
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
|
|
||||||
|
|
||||||
if (frameType == FRAME_TYPE_PFRAME) {
|
|
||||||
// P-frame
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
|
|
||||||
} else {
|
|
||||||
// I-frame
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
|
|
||||||
}
|
|
||||||
|
|
||||||
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
|
|
||||||
|
|
||||||
CFRelease(sampleBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void renderFrame(SDL_UserEvent* event) override
|
|
||||||
{
|
|
||||||
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void dropFrame(SDL_UserEvent* event) override
|
|
||||||
{
|
|
||||||
// Submit anyway and let the drop happen in the render pipeline
|
|
||||||
submitBuffer((CMSampleBufferRef)event->data1, (int)(long)event->data2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void setupDisplayLayer()
|
|
||||||
{
|
|
||||||
CALayer* oldLayer = m_DisplayLayer;
|
|
||||||
|
|
||||||
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
|
||||||
m_DisplayLayer.bounds = m_View.bounds;
|
|
||||||
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
|
|
||||||
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
|
||||||
|
|
||||||
CALayer* viewLayer = m_View.layer;
|
|
||||||
if (oldLayer != nil) {
|
|
||||||
[viewLayer replaceSublayer:oldLayer with:m_DisplayLayer];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[viewLayer addSublayer:m_DisplayLayer];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for another IDR frame
|
|
||||||
m_NeedsIdr = true;
|
|
||||||
|
|
||||||
delete m_SpsData;
|
|
||||||
delete m_PpsData;
|
|
||||||
delete m_VpsData;
|
|
||||||
|
|
||||||
m_SpsData = m_PpsData = m_VpsData = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateBufferForRange(CMBlockBufferRef existingBuffer, unsigned char* data, int offset, int nalLength)
|
|
||||||
{
|
|
||||||
OSStatus status;
|
|
||||||
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
|
|
||||||
|
|
||||||
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
|
|
||||||
// so it can handle freeing it when the block buffer is destroyed
|
|
||||||
if (offset == 1) {
|
|
||||||
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
|
||||||
|
|
||||||
// Pass the real buffer pointer directly (no offset)
|
|
||||||
// This will give it to the block buffer to free when it's released.
|
|
||||||
// All further calls to CMBlockBufferAppendMemoryBlock will do so
|
|
||||||
// at an offset and will not be asking the buffer to be freed.
|
|
||||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
|
|
||||||
nalLength + 1, // Add 1 for the offset we decremented
|
|
||||||
kCFAllocatorDefault,
|
|
||||||
NULL, 0, nalLength + 1, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferReplaceDataBytes failed: %d",
|
|
||||||
(int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the length prefix to existing buffer
|
|
||||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
|
||||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
|
||||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
|
|
||||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferReplaceDataBytes failed: %d",
|
|
||||||
(int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Append a 4 byte buffer to this block for the length prefix
|
|
||||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE,
|
|
||||||
kCFAllocatorDefault, NULL, 0,
|
|
||||||
NAL_LENGTH_PREFIX_SIZE, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferAppendMemoryBlock failed: %d",
|
|
||||||
(int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the length prefix to the new buffer
|
|
||||||
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
|
||||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
|
||||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
|
||||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
|
|
||||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferReplaceDataBytes failed: %d",
|
|
||||||
(int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach the buffer by reference to the block buffer
|
|
||||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
|
|
||||||
dataLength,
|
|
||||||
kCFAllocatorNull, // Don't deallocate data on free
|
|
||||||
NULL, 0, dataLength, 0);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMBlockBufferReplaceDataBytes failed: %d",
|
|
||||||
(int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
|
||||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
|
||||||
int m_VideoFormat;
|
|
||||||
QByteArray* m_SpsData;
|
|
||||||
QByteArray* m_PpsData;
|
|
||||||
QByteArray* m_VpsData;
|
|
||||||
NSView* m_View;
|
|
||||||
bool m_NeedsIdr;
|
|
||||||
CVDisplayLinkRef m_DisplayLink;
|
|
||||||
uint64_t m_VsyncTime, m_LastVsyncTime;
|
|
||||||
int m_FrameCountOnVsync[FUTURE_FRAMES_IN_VSYNC];
|
|
||||||
SDL_SpinLock m_VsyncStatsLock;
|
|
||||||
};
|
|
||||||
|
|
||||||
IVideoDecoder* AVFoundationDecoderFactory::createDecoder() {
|
|
||||||
return new AVFoundationDecoder();
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,10 @@ public:
|
|||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int frameRate) = 0;
|
int frameRate,
|
||||||
|
bool enableVsync) = 0;
|
||||||
virtual bool isHardwareAccelerated() = 0;
|
virtual bool isHardwareAccelerated() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
#include <Initguid.h>
|
#include <initguid.h>
|
||||||
#include "dxva2.h"
|
#include "dxva2.h"
|
||||||
|
#include "../ffmpeg.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <VersionHelpers.h>
|
||||||
|
#include <dwmapi.h>
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -8,23 +17,27 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
|
|||||||
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
||||||
|
|
||||||
DXVA2Renderer::DXVA2Renderer() :
|
DXVA2Renderer::DXVA2Renderer() :
|
||||||
m_FrameIndex(0),
|
|
||||||
m_SurfacesUsed(0),
|
|
||||||
m_SdlRenderer(nullptr),
|
|
||||||
m_DecService(nullptr),
|
m_DecService(nullptr),
|
||||||
m_Decoder(nullptr),
|
m_Decoder(nullptr),
|
||||||
|
m_SurfacesUsed(0),
|
||||||
m_Pool(nullptr),
|
m_Pool(nullptr),
|
||||||
m_Device(nullptr),
|
m_Device(nullptr),
|
||||||
m_RenderTarget(nullptr),
|
m_RenderTarget(nullptr),
|
||||||
m_ProcService(nullptr),
|
m_ProcService(nullptr),
|
||||||
m_Processor(nullptr)
|
m_Processor(nullptr),
|
||||||
|
m_FrameIndex(0)
|
||||||
{
|
{
|
||||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||||
|
|
||||||
|
// Use MMCSS scheduling for lower scheduling latency while we're streaming
|
||||||
|
DwmEnableMMCSS(TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
DXVA2Renderer::~DXVA2Renderer()
|
DXVA2Renderer::~DXVA2Renderer()
|
||||||
{
|
{
|
||||||
|
DwmEnableMMCSS(FALSE);
|
||||||
|
|
||||||
SAFE_COM_RELEASE(m_DecService);
|
SAFE_COM_RELEASE(m_DecService);
|
||||||
SAFE_COM_RELEASE(m_Decoder);
|
SAFE_COM_RELEASE(m_Decoder);
|
||||||
SAFE_COM_RELEASE(m_Device);
|
SAFE_COM_RELEASE(m_Device);
|
||||||
@@ -39,10 +52,6 @@ DXVA2Renderer::~DXVA2Renderer()
|
|||||||
if (m_Pool != nullptr) {
|
if (m_Pool != nullptr) {
|
||||||
av_buffer_pool_uninit(&m_Pool);
|
av_buffer_pool_uninit(&m_Pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_SdlRenderer != nullptr) {
|
|
||||||
SDL_DestroyRenderer(m_SdlRenderer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*)
|
void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*)
|
||||||
@@ -75,11 +84,8 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
|
|
||||||
context->hwaccel_context = &m_DXVAContext;
|
context->hwaccel_context = &m_DXVAContext;
|
||||||
|
|
||||||
context->get_format = ffGetFormat;
|
|
||||||
context->get_buffer2 = ffGetBuffer2;
|
context->get_buffer2 = ffGetBuffer2;
|
||||||
|
|
||||||
context->opaque = this;
|
|
||||||
|
|
||||||
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
|
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
|
||||||
if (!m_Pool) {
|
if (!m_Pool) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -87,12 +93,15 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using DXVA2 accelerated renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
||||||
{
|
{
|
||||||
DXVA2Renderer* me = reinterpret_cast<DXVA2Renderer*>(context->opaque);
|
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
|
||||||
|
|
||||||
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
||||||
if (!frame->buf[0]) {
|
if (!frame->buf[0]) {
|
||||||
@@ -101,31 +110,20 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
|||||||
|
|
||||||
frame->data[3] = frame->buf[0]->data;
|
frame->data[3] = frame->buf[0]->data;
|
||||||
frame->format = AV_PIX_FMT_DXVA2_VLD;
|
frame->format = AV_PIX_FMT_DXVA2_VLD;
|
||||||
frame->width = me->m_Width;
|
frame->width = me->m_VideoWidth;
|
||||||
frame->height = me->m_Height;
|
frame->height = me->m_VideoHeight;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AVPixelFormat DXVA2Renderer::ffGetFormat(AVCodecContext*,
|
|
||||||
const enum AVPixelFormat* pixFmts)
|
|
||||||
{
|
|
||||||
const enum AVPixelFormat *p;
|
|
||||||
|
|
||||||
for (p = pixFmts; *p != -1; p++) {
|
|
||||||
if (*p == AV_PIX_FMT_DXVA2_VLD)
|
|
||||||
return *p;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to find DXVA2 HW surface format");
|
|
||||||
return AV_PIX_FMT_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DXVA2Renderer::initializeDecoder()
|
bool DXVA2Renderer::initializeDecoder()
|
||||||
{
|
{
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
|
if (isDecoderBlacklisted()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
|
||||||
reinterpret_cast<void**>(&m_DecService));
|
reinterpret_cast<void**>(&m_DecService));
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
@@ -208,18 +206,11 @@ bool DXVA2Renderer::initializeDecoder()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int alignment;
|
// Alignment was already taken care of
|
||||||
|
SDL_assert(m_Desc.SampleWidth % 16 == 0);
|
||||||
// HEVC using DXVA requires 128B alignment
|
SDL_assert(m_Desc.SampleHeight % 16 == 0);
|
||||||
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
hr = m_DecService->CreateSurface(m_Desc.SampleWidth,
|
||||||
alignment = 128;
|
m_Desc.SampleHeight,
|
||||||
}
|
|
||||||
else {
|
|
||||||
alignment = 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr = m_DecService->CreateSurface(FFALIGN(m_Width, alignment),
|
|
||||||
FFALIGN(m_Height, alignment),
|
|
||||||
ARRAYSIZE(m_DecSurfaces) - 1,
|
ARRAYSIZE(m_DecSurfaces) - 1,
|
||||||
m_Desc.Format,
|
m_Desc.Format,
|
||||||
D3DPOOL_DEFAULT,
|
D3DPOOL_DEFAULT,
|
||||||
@@ -262,6 +253,9 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
D3DSURFACE_DESC renderTargetDesc;
|
D3DSURFACE_DESC renderTargetDesc;
|
||||||
m_RenderTarget->GetDesc(&renderTargetDesc);
|
m_RenderTarget->GetDesc(&renderTargetDesc);
|
||||||
|
|
||||||
|
m_DisplayWidth = renderTargetDesc.Width;
|
||||||
|
m_DisplayHeight = renderTargetDesc.Height;
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
||||||
reinterpret_cast<void**>(&m_ProcService));
|
reinterpret_cast<void**>(&m_ProcService));
|
||||||
|
|
||||||
@@ -302,6 +296,14 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
||||||
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Device %d can't stretch video: %x",
|
||||||
|
i,
|
||||||
|
caps.VideoProcessorOperations);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
||||||
@@ -338,45 +340,302 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
|
bool DXVA2Renderer::isDecoderBlacklisted()
|
||||||
{
|
{
|
||||||
m_VideoFormat = videoFormat;
|
IDirect3D9* d3d9;
|
||||||
m_Width = width;
|
HRESULT hr;
|
||||||
m_Height = height;
|
bool result = false;
|
||||||
|
|
||||||
// FFmpeg will be decoding on different threads than the main thread that we're
|
// TODO: Update for HEVC Main10
|
||||||
// currently running on right now. We must set this hint so SDL will pass
|
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
|
||||||
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
|
|
||||||
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
|
|
||||||
|
|
||||||
// We want VSYNC when running in full-screen. DWM will give us
|
hr = m_Device->GetDirect3D(&d3d9);
|
||||||
// tear-free video when running composited (windowed) without any
|
if (SUCCEEDED(hr)) {
|
||||||
// extra latency waiting for VSYNC.
|
D3DCAPS9 caps;
|
||||||
m_SdlRenderer = SDL_CreateRenderer(window, -1,
|
|
||||||
SDL_RENDERER_ACCELERATED |
|
hr = m_Device->GetDeviceCaps(&caps);
|
||||||
((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) ?
|
if (SUCCEEDED(hr)) {
|
||||||
SDL_RENDERER_PRESENTVSYNC : 0));
|
D3DADAPTER_IDENTIFIER9 id;
|
||||||
if (!m_SdlRenderer) {
|
|
||||||
|
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected GPU: %s (%x:%x)",
|
||||||
|
id.Description,
|
||||||
|
id.VendorId,
|
||||||
|
id.DeviceId);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU driver: %s %d.%d.%d.%d",
|
||||||
|
id.Driver,
|
||||||
|
HIWORD(id.DriverVersion.HighPart),
|
||||||
|
LOWORD(id.DriverVersion.HighPart),
|
||||||
|
HIWORD(id.DriverVersion.LowPart),
|
||||||
|
LOWORD(id.DriverVersion.LowPart));
|
||||||
|
|
||||||
|
if (id.VendorId == 0x8086) {
|
||||||
|
// Intel seems to encode the series in the high byte of
|
||||||
|
// the device ID. We want to avoid the "Partial" acceleration
|
||||||
|
// support explicitly. Those will claim to have HW acceleration
|
||||||
|
// but perform badly.
|
||||||
|
// https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)
|
||||||
|
// https://raw.githubusercontent.com/GameTechDev/gpudetect/master/IntelGfx.cfg
|
||||||
|
switch (id.DeviceId & 0xFF00) {
|
||||||
|
case 0x0400: // Haswell
|
||||||
|
case 0x0A00: // Haswell
|
||||||
|
case 0x0D00: // Haswell
|
||||||
|
case 0x1600: // Broadwell
|
||||||
|
case 0x2200: // Cherry Trail and Braswell
|
||||||
|
// Blacklist these for HEVC to avoid hybrid decode
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for HEVC due to hybrid decode");
|
||||||
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
|
||||||
|
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
|
||||||
|
// is not affected on my Intel HD 515.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/32
|
||||||
|
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
|
||||||
|
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Intel driver version blacklisted for HEVC");
|
||||||
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Everything else is fine with whatever it says it supports
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (id.VendorId == 0x10DE) {
|
||||||
|
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
|
||||||
|
// for HEVC decoding, since that's hybrid. It appears that Kepler GPUs
|
||||||
|
// also had some hybrid decode support (per DXVA2 Checker) so we'll
|
||||||
|
// blacklist those too.
|
||||||
|
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
|
||||||
|
// https://bluesky23.yukishigure.com/en/dxvac/deviceInfo/decoder.html
|
||||||
|
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
|
||||||
|
if ((id.DeviceId >= 0x1180 && id.DeviceId <= 0x11BF) || // GK104
|
||||||
|
(id.DeviceId >= 0x11C0 && id.DeviceId <= 0x11FF) || // GK106
|
||||||
|
(id.DeviceId >= 0x0FC0 && id.DeviceId <= 0x0FFF) || // GK107
|
||||||
|
(id.DeviceId >= 0x1000 && id.DeviceId <= 0x103F) || // GK110/GK110B
|
||||||
|
(id.DeviceId >= 0x1280 && id.DeviceId <= 0x12BF) || // GK208
|
||||||
|
(id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
|
||||||
|
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
|
||||||
|
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
|
||||||
|
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
|
||||||
|
(id.DeviceId == 0x1667) || // GM204
|
||||||
|
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
|
||||||
|
// Avoid HEVC on Feature Set E GPUs
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for HEVC due to hybrid decode");
|
||||||
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (id.VendorId == 0x1002) {
|
||||||
|
// AMD doesn't seem to do hybrid acceleration?
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unrecognized vendor ID: %x",
|
||||||
|
id.VendorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetDeviceCaps() failed: %x", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d9->Release();
|
||||||
|
}
|
||||||
|
else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateRenderer() failed: %s",
|
"GetDirect3D() failed: %x", hr);
|
||||||
SDL_GetError());
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for format %x",
|
||||||
|
m_VideoFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
SDL_GetWindowWMInfo(window, &info);
|
||||||
|
|
||||||
|
IDirect3D9Ex* d3d9ex;
|
||||||
|
HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Direct3DCreate9Ex() failed: %x",
|
||||||
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
|
int adapterIndex = SDL_Direct3D9GetAdapterIndex(SDL_GetWindowDisplayIndex(window));
|
||||||
|
Uint32 windowFlags = SDL_GetWindowFlags(window);
|
||||||
|
|
||||||
|
D3DCAPS9 deviceCaps;
|
||||||
|
d3d9ex->GetDeviceCaps(adapterIndex, D3DDEVTYPE_HAL, &deviceCaps);
|
||||||
|
|
||||||
|
D3DDISPLAYMODEEX currentMode;
|
||||||
|
currentMode.Size = sizeof(currentMode);
|
||||||
|
d3d9ex->GetAdapterDisplayModeEx(adapterIndex, ¤tMode, nullptr);
|
||||||
|
|
||||||
|
D3DPRESENT_PARAMETERS d3dpp = {};
|
||||||
|
d3dpp.hDeviceWindow = info.info.win.window;
|
||||||
|
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
||||||
|
|
||||||
|
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
d3dpp.Windowed = false;
|
||||||
|
d3dpp.BackBufferWidth = currentMode.Width;
|
||||||
|
d3dpp.BackBufferHeight = currentMode.Height;
|
||||||
|
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
|
||||||
|
d3dpp.BackBufferFormat = currentMode.Format;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d3dpp.Windowed = true;
|
||||||
|
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
|
||||||
|
|
||||||
|
SDL_GetWindowSize(window, (int*)&d3dpp.BackBufferWidth, (int*)&d3dpp.BackBufferHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL dwmEnabled;
|
||||||
|
DwmIsCompositionEnabled(&dwmEnabled);
|
||||||
|
if (d3dpp.Windowed && dwmEnabled) {
|
||||||
|
// If composition enabled, disable v-sync and let DWM manage things
|
||||||
|
// to reduce latency by avoiding double v-syncing.
|
||||||
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
|
||||||
|
// If V-sync is enabled (not rendering faster than display),
|
||||||
|
// we can use FlipEx for more efficient swapping.
|
||||||
|
if (enableVsync) {
|
||||||
|
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
||||||
|
// continue while DWM is waiting to render the surface to the display.
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
||||||
|
d3dpp.BackBufferCount = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// With V-sync off, we won't use FlipEx because that will block while
|
||||||
|
// DWM is waiting to render our surface (effectively behaving like V-Sync).
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
d3dpp.BackBufferCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Windowed mode with DWM running");
|
||||||
|
}
|
||||||
|
else if (enableVsync) {
|
||||||
|
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
||||||
|
// We will enable V-sync in this scenario to avoid tearing.
|
||||||
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-Sync enabled");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
||||||
|
// We will allowing tearing for lowest latency.
|
||||||
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-Sync disabled in tearing mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Windowed: %d | Present Interval: %x",
|
||||||
|
d3dpp.Windowed, d3dpp.PresentationInterval);
|
||||||
|
|
||||||
|
// FFmpeg requires this attribute for doing asynchronous decoding
|
||||||
|
// in a separate thread with this device.
|
||||||
|
int deviceFlags = D3DCREATE_MULTITHREADED;
|
||||||
|
|
||||||
|
if (deviceCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
|
||||||
|
deviceFlags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No hardware vertex processing support!");
|
||||||
|
deviceFlags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = d3d9ex->CreateDeviceEx(adapterIndex,
|
||||||
|
D3DDEVTYPE_HAL,
|
||||||
|
d3dpp.hDeviceWindow,
|
||||||
|
deviceFlags,
|
||||||
|
&d3dpp,
|
||||||
|
d3dpp.Windowed ? nullptr : ¤tMode,
|
||||||
|
&m_Device);
|
||||||
|
|
||||||
|
d3d9ex->Release();
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CreateDeviceEx() failed: %x",
|
||||||
|
hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_Device->SetMaximumFrameLatency(1);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SetMaximumFrameLatency() failed: %x",
|
||||||
|
hr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
|
||||||
|
{
|
||||||
|
m_VideoFormat = videoFormat;
|
||||||
|
m_VideoWidth = width;
|
||||||
|
m_VideoHeight = height;
|
||||||
|
|
||||||
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
||||||
m_Desc.SampleWidth = m_Width;
|
|
||||||
m_Desc.SampleHeight = m_Height;
|
int alignment;
|
||||||
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_ProgressiveChroma;
|
|
||||||
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
|
// HEVC using DXVA requires 128B alignment
|
||||||
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
|
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_dim;
|
alignment = 128;
|
||||||
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
|
}
|
||||||
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
|
else {
|
||||||
|
alignment = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Desc.SampleWidth = FFALIGN(m_VideoWidth, alignment);
|
||||||
|
m_Desc.SampleHeight = FFALIGN(m_VideoHeight, alignment);
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
|
||||||
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoLighting = DXVA2_VideoLighting_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
||||||
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||||
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
||||||
|
|
||||||
|
if (!initializeDevice(window, enableVsync)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!initializeDecoder()) {
|
if (!initializeDecoder()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -385,51 +644,158 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
|
||||||
|
// When focus is lost, the window is minimized then immediately restored without
|
||||||
|
// input focus. This glitches out the renderer and a bunch of other stuff.
|
||||||
|
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
||||||
|
// own, so just disable SDL's handling of the focus loss event.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrame(AVFrame* frame)
|
bool DXVA2Renderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
// We validate the DXVA2 profiles are supported
|
||||||
|
// in initialize() so no test frame is required
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DXVA2Renderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||||
{
|
{
|
||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
hr = m_Device->TestCooperativeLevel();
|
switch (frame->color_range) {
|
||||||
switch (hr) {
|
case AVCOL_RANGE_JPEG:
|
||||||
case D3D_OK:
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_0_255;
|
||||||
|
break;
|
||||||
|
case AVCOL_RANGE_MPEG:
|
||||||
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_16_235;
|
||||||
break;
|
break;
|
||||||
case D3DERR_DEVICELOST:
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"D3D device lost!");
|
|
||||||
return;
|
|
||||||
case D3DERR_DEVICENOTRESET:
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"D3D device not reset!");
|
|
||||||
return;
|
|
||||||
default:
|
default:
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
m_Desc.SampleFormat.NominalRange = DXVA2_NominalRange_Unknown;
|
||||||
"Unknown D3D error: %x",
|
break;
|
||||||
hr);
|
}
|
||||||
return;
|
|
||||||
|
switch (frame->color_primaries) {
|
||||||
|
case AVCOL_PRI_BT709:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT709;
|
||||||
|
break;
|
||||||
|
case AVCOL_PRI_BT470M:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysM;
|
||||||
|
break;
|
||||||
|
case AVCOL_PRI_BT470BG:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_BT470_2_SysBG;
|
||||||
|
break;
|
||||||
|
case AVCOL_PRI_SMPTE170M:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE170M;
|
||||||
|
break;
|
||||||
|
case AVCOL_PRI_SMPTE240M:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_SMPTE240M;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (frame->color_trc) {
|
||||||
|
case AVCOL_TRC_SMPTE170M:
|
||||||
|
case AVCOL_TRC_BT709:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_709;
|
||||||
|
break;
|
||||||
|
case AVCOL_TRC_LINEAR:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_10;
|
||||||
|
break;
|
||||||
|
case AVCOL_TRC_GAMMA22:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_22;
|
||||||
|
break;
|
||||||
|
case AVCOL_TRC_GAMMA28:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_28;
|
||||||
|
break;
|
||||||
|
case AVCOL_TRC_SMPTE240M:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_240M;
|
||||||
|
break;
|
||||||
|
case AVCOL_TRC_IEC61966_2_1:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_sRGB;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (frame->colorspace) {
|
||||||
|
case AVCOL_SPC_BT709:
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT709;
|
||||||
|
break;
|
||||||
|
case AVCOL_SPC_BT470BG:
|
||||||
|
case AVCOL_SPC_SMPTE170M:
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_BT601;
|
||||||
|
break;
|
||||||
|
case AVCOL_SPC_SMPTE240M:
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_SMPTE240M;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_Desc.SampleFormat.VideoTransferMatrix = DXVA2_VideoTransferMatrix_Unknown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (frame->chroma_location) {
|
||||||
|
case AVCHROMA_LOC_LEFT:
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
|
||||||
|
DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
|
||||||
|
break;
|
||||||
|
case AVCHROMA_LOC_CENTER:
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Vertically_AlignedChromaPlanes;
|
||||||
|
break;
|
||||||
|
case AVCHROMA_LOC_TOPLEFT:
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Horizontally_Cosited |
|
||||||
|
DXVA2_VideoChromaSubsampling_Vertically_Cosited;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
m_Desc.SampleFormat.VideoChromaSubsampling = DXVA2_VideoChromaSubsampling_Unknown;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
DXVA2_VideoSample sample = {};
|
DXVA2_VideoSample sample = {};
|
||||||
sample.Start = m_FrameIndex;
|
sample.Start = m_FrameIndex;
|
||||||
sample.End = m_FrameIndex + 1;
|
sample.End = m_FrameIndex + 1;
|
||||||
sample.SrcSurface = surface;
|
sample.SrcSurface = surface;
|
||||||
sample.SrcRect.right = m_Desc.SampleWidth;
|
sample.SrcRect.right = m_VideoWidth;
|
||||||
sample.SrcRect.bottom = m_Desc.SampleHeight;
|
sample.SrcRect.bottom = m_VideoHeight;
|
||||||
sample.DstRect = sample.SrcRect;
|
|
||||||
sample.SampleFormat = m_Desc.SampleFormat;
|
sample.SampleFormat = m_Desc.SampleFormat;
|
||||||
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
|
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
|
||||||
|
|
||||||
|
// Center in frame and preserve aspect ratio
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
sample.DstRect.left = dst.x;
|
||||||
|
sample.DstRect.right = dst.x + dst.w;
|
||||||
|
sample.DstRect.top = dst.y;
|
||||||
|
sample.DstRect.bottom = dst.y + dst.h;
|
||||||
|
|
||||||
DXVA2_VideoProcessBltParams bltParams = {};
|
DXVA2_VideoProcessBltParams bltParams = {};
|
||||||
|
|
||||||
bltParams.TargetFrame = m_FrameIndex++;
|
bltParams.TargetFrame = m_FrameIndex++;
|
||||||
bltParams.TargetRect.right = m_Desc.SampleWidth;
|
bltParams.TargetRect = sample.DstRect;
|
||||||
bltParams.TargetRect.bottom = m_Desc.SampleHeight;
|
|
||||||
bltParams.BackgroundColor.Y = 0x1000;
|
|
||||||
bltParams.BackgroundColor.Cb = 0x8000;
|
|
||||||
bltParams.BackgroundColor.Cr = 0x8000;
|
|
||||||
bltParams.BackgroundColor.Alpha = 0xFFFF;
|
bltParams.BackgroundColor.Alpha = 0xFFFF;
|
||||||
|
|
||||||
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
bltParams.DestFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||||
@@ -441,12 +807,27 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
|
|||||||
|
|
||||||
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
||||||
|
|
||||||
|
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
||||||
|
|
||||||
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"VideoProcessBlt() failed: %x",
|
"VideoProcessBlt() failed: %x",
|
||||||
hr);
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Device->Present(nullptr, nullptr, nullptr, nullptr);
|
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"PresentEx() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
|
#include "pacer/pacer.h"
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#include <dxva2api.h>
|
#include <dxva2api.h>
|
||||||
@@ -17,13 +18,20 @@ public:
|
|||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height);
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync);
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrame(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
bool initializeRenderer();
|
bool initializeRenderer();
|
||||||
|
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||||
|
bool isDecoderBlacklisted();
|
||||||
|
|
||||||
static
|
static
|
||||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||||
@@ -34,15 +42,12 @@ private:
|
|||||||
static
|
static
|
||||||
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
|
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
|
||||||
|
|
||||||
static
|
|
||||||
enum AVPixelFormat ffGetFormat(AVCodecContext*,
|
|
||||||
const enum AVPixelFormat* pixFmts);
|
|
||||||
|
|
||||||
int m_VideoFormat;
|
int m_VideoFormat;
|
||||||
int m_Width;
|
int m_VideoWidth;
|
||||||
int m_Height;
|
int m_VideoHeight;
|
||||||
|
|
||||||
SDL_Renderer* m_SdlRenderer;
|
int m_DisplayWidth;
|
||||||
|
int m_DisplayHeight;
|
||||||
|
|
||||||
struct dxva_context m_DXVAContext;
|
struct dxva_context m_DXVAContext;
|
||||||
IDirect3DSurface9* m_DecSurfaces[19];
|
IDirect3DSurface9* m_DecSurfaces[19];
|
||||||
@@ -52,7 +57,7 @@ private:
|
|||||||
int m_SurfacesUsed;
|
int m_SurfacesUsed;
|
||||||
AVBufferPool* m_Pool;
|
AVBufferPool* m_Pool;
|
||||||
|
|
||||||
IDirect3DDevice9* m_Device;
|
IDirect3DDevice9Ex* m_Device;
|
||||||
IDirect3DSurface9* m_RenderTarget;
|
IDirect3DSurface9* m_RenderTarget;
|
||||||
IDirectXVideoProcessorService* m_ProcService;
|
IDirectXVideoProcessorService* m_ProcService;
|
||||||
IDirectXVideoProcessor* m_Processor;
|
IDirectXVideoProcessor* m_Processor;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pacer.h"
|
||||||
|
|
||||||
|
class DisplayLinkVsyncSourceFactory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static
|
||||||
|
IVsyncSource* createVsyncSource(Pacer* pacer);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "displaylinkvsyncsource.h"
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#include <CoreVideo/CoreVideo.h>
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
class DisplayLinkVsyncSource : public IVsyncSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DisplayLinkVsyncSource(Pacer* pacer)
|
||||||
|
: m_Pacer(pacer),
|
||||||
|
m_DisplayLink(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~DisplayLinkVsyncSource() override
|
||||||
|
{
|
||||||
|
if (m_DisplayLink != nullptr) {
|
||||||
|
CVDisplayLinkStop(m_DisplayLink);
|
||||||
|
CVDisplayLinkRelease(m_DisplayLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CGDirectDisplayID
|
||||||
|
getDisplayID(NSScreen* screen)
|
||||||
|
{
|
||||||
|
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
|
||||||
|
return [screenNumber unsignedIntValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CVReturn
|
||||||
|
displayLinkOutputCallback(
|
||||||
|
CVDisplayLinkRef displayLink,
|
||||||
|
const CVTimeStamp* /* now */,
|
||||||
|
const CVTimeStamp* /* vsyncTime */,
|
||||||
|
CVOptionFlags,
|
||||||
|
CVOptionFlags*,
|
||||||
|
void *displayLinkContext)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
||||||
|
|
||||||
|
SDL_assert(displayLink == me->m_DisplayLink);
|
||||||
|
|
||||||
|
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
||||||
|
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
||||||
|
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
||||||
|
// draw using the current host time, rather than the actual v-sync target
|
||||||
|
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
||||||
|
// interval, even if many ms prior, we can safely use the current host time
|
||||||
|
// and get a consistent callback for each v-sync. This reduces video latency
|
||||||
|
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
||||||
|
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
||||||
|
|
||||||
|
return kCVReturnSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps) override
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
|
||||||
|
|
||||||
|
m_DisplayFps = displayFps;
|
||||||
|
|
||||||
|
NSScreen* screen = [info.info.cocoa.window screen];
|
||||||
|
CVReturn status;
|
||||||
|
if (screen == nullptr) {
|
||||||
|
// Window not visible on any display, so use a
|
||||||
|
// CVDisplayLink that can work with all active displays.
|
||||||
|
// When we become visible, we'll recreate ourselves
|
||||||
|
// and associate with the new screen.
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow is not visible on any display");
|
||||||
|
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
CGDirectDisplayID displayId;
|
||||||
|
displayId = getDisplayID(screen);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow on display: %x",
|
||||||
|
displayId);
|
||||||
|
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
|
||||||
|
}
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create CVDisplayLink: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkSetOutputCallback() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkStart(m_DisplayLink);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkStart() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
CVDisplayLinkRef m_DisplayLink;
|
||||||
|
int m_DisplayFps;
|
||||||
|
};
|
||||||
|
|
||||||
|
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
|
||||||
|
return new DisplayLinkVsyncSource(pacer);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
#include "dxvsyncsource.h"
|
||||||
|
|
||||||
|
// Useful references:
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=467617
|
||||||
|
// https://chromium.googlesource.com/chromium/src.git/+/c564f2fe339b2b2abb0c8773c90c83215670ea71/gpu/ipc/service/gpu_vsync_provider_win.cc
|
||||||
|
|
||||||
|
DxVsyncSource::DxVsyncSource(Pacer* pacer) :
|
||||||
|
m_Pacer(pacer),
|
||||||
|
m_Thread(nullptr),
|
||||||
|
m_Gdi32Handle(nullptr)
|
||||||
|
{
|
||||||
|
SDL_AtomicSet(&m_Stopping, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
DxVsyncSource::~DxVsyncSource()
|
||||||
|
{
|
||||||
|
if (m_Thread != nullptr) {
|
||||||
|
SDL_AtomicSet(&m_Stopping, 1);
|
||||||
|
SDL_WaitThread(m_Thread, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Gdi32Handle != nullptr) {
|
||||||
|
FreeLibrary(m_Gdi32Handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
|
||||||
|
{
|
||||||
|
m_DisplayFps = displayFps;
|
||||||
|
|
||||||
|
m_Gdi32Handle = LoadLibraryA("gdi32.dll");
|
||||||
|
if (m_Gdi32Handle == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to load gdi32.dll: %d",
|
||||||
|
GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_D3DKMTOpenAdapterFromHdc = (PFND3DKMTOPENADAPTERFROMHDC)GetProcAddress(m_Gdi32Handle, "D3DKMTOpenAdapterFromHdc");
|
||||||
|
m_D3DKMTCloseAdapter = (PFND3DKMTCLOSEADAPTER)GetProcAddress(m_Gdi32Handle, "D3DKMTCloseAdapter");
|
||||||
|
m_D3DKMTWaitForVerticalBlankEvent = (PFND3DKMTWAITFORVERTICALBLANKEVENT)GetProcAddress(m_Gdi32Handle, "D3DKMTWaitForVerticalBlankEvent");
|
||||||
|
|
||||||
|
if (m_D3DKMTOpenAdapterFromHdc == nullptr ||
|
||||||
|
m_D3DKMTCloseAdapter == nullptr ||
|
||||||
|
m_D3DKMTWaitForVerticalBlankEvent == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Missing required function in gdi32.dll");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Window = info.info.win.window;
|
||||||
|
|
||||||
|
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
|
||||||
|
if (m_Thread == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to create DX V-sync thread: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DxVsyncSource::vsyncThread(void* context)
|
||||||
|
{
|
||||||
|
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
|
||||||
|
|
||||||
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||||
|
|
||||||
|
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
|
||||||
|
HMONITOR lastMonitor = nullptr;
|
||||||
|
DEVMODEA monitorMode;
|
||||||
|
monitorMode.dmSize = sizeof(monitorMode);
|
||||||
|
|
||||||
|
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
||||||
|
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
|
||||||
|
NTSTATUS status;
|
||||||
|
|
||||||
|
// If the monitor has changed from last time, open the new adapter
|
||||||
|
HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
|
||||||
|
if (currentMonitor != lastMonitor) {
|
||||||
|
MONITORINFOEXA monitorInfo = {};
|
||||||
|
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||||
|
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetMonitorInfo() failed: %d",
|
||||||
|
GetLastError());
|
||||||
|
SDL_Delay(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"EnumDisplaySettings() failed: %d",
|
||||||
|
GetLastError());
|
||||||
|
SDL_Delay(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Monitor changed: %s %d Hz",
|
||||||
|
monitorInfo.szDevice,
|
||||||
|
monitorMode.dmDisplayFrequency);
|
||||||
|
|
||||||
|
if (openAdapterParams.hAdapter != 0) {
|
||||||
|
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
|
||||||
|
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
|
||||||
|
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
|
||||||
|
if (!openAdapterParams.hDc) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CreateDC() failed: %d",
|
||||||
|
GetLastError());
|
||||||
|
SDL_Delay(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
|
||||||
|
DeleteDC(openAdapterParams.hDc);
|
||||||
|
|
||||||
|
if (status != STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DKMTOpenAdapterFromHdc() failed: %x",
|
||||||
|
status);
|
||||||
|
SDL_Delay(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMonitor = currentMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
|
||||||
|
waitForVblankEventParams.hDevice = 0;
|
||||||
|
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
|
||||||
|
|
||||||
|
status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
|
||||||
|
if (status != STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
|
||||||
|
status);
|
||||||
|
SDL_Delay(10);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openAdapterParams.hAdapter != 0) {
|
||||||
|
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
|
||||||
|
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
|
||||||
|
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pacer.h"
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
// from <D3dkmthk.h>
|
||||||
|
typedef LONG NTSTATUS;
|
||||||
|
typedef UINT D3DKMT_HANDLE;
|
||||||
|
typedef UINT D3DDDI_VIDEO_PRESENT_SOURCE_ID;
|
||||||
|
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
|
||||||
|
#define STATUS_GRAPHICS_PRESENT_OCCLUDED ((NTSTATUS)0xC01E0006L)
|
||||||
|
typedef struct _D3DKMT_OPENADAPTERFROMHDC {
|
||||||
|
HDC hDc;
|
||||||
|
D3DKMT_HANDLE hAdapter;
|
||||||
|
LUID AdapterLuid;
|
||||||
|
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
|
||||||
|
} D3DKMT_OPENADAPTERFROMHDC;
|
||||||
|
typedef struct _D3DKMT_CLOSEADAPTER {
|
||||||
|
D3DKMT_HANDLE hAdapter;
|
||||||
|
} D3DKMT_CLOSEADAPTER;
|
||||||
|
typedef struct _D3DKMT_WAITFORVERTICALBLANKEVENT {
|
||||||
|
D3DKMT_HANDLE hAdapter;
|
||||||
|
D3DKMT_HANDLE hDevice;
|
||||||
|
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
|
||||||
|
} D3DKMT_WAITFORVERTICALBLANKEVENT;
|
||||||
|
typedef NTSTATUS(APIENTRY* PFND3DKMTOPENADAPTERFROMHDC)(D3DKMT_OPENADAPTERFROMHDC*);
|
||||||
|
typedef NTSTATUS(APIENTRY* PFND3DKMTCLOSEADAPTER)(D3DKMT_CLOSEADAPTER*);
|
||||||
|
typedef NTSTATUS(APIENTRY* PFND3DKMTWAITFORVERTICALBLANKEVENT)(D3DKMT_WAITFORVERTICALBLANKEVENT*);
|
||||||
|
|
||||||
|
class DxVsyncSource : public IVsyncSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DxVsyncSource(Pacer* pacer);
|
||||||
|
|
||||||
|
virtual ~DxVsyncSource();
|
||||||
|
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int vsyncThread(void* context);
|
||||||
|
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
SDL_Thread* m_Thread;
|
||||||
|
SDL_atomic_t m_Stopping;
|
||||||
|
HMODULE m_Gdi32Handle;
|
||||||
|
HWND m_Window;
|
||||||
|
int m_DisplayFps;
|
||||||
|
|
||||||
|
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
|
||||||
|
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;
|
||||||
|
PFND3DKMTWAITFORVERTICALBLANKEVENT m_D3DKMTWaitForVerticalBlankEvent;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include "nullthreadedvsyncsource.h"
|
||||||
|
|
||||||
|
NullThreadedVsyncSource::NullThreadedVsyncSource(Pacer* pacer) :
|
||||||
|
m_Pacer(pacer),
|
||||||
|
m_Thread(nullptr)
|
||||||
|
{
|
||||||
|
SDL_AtomicSet(&m_Stopping, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NullThreadedVsyncSource::~NullThreadedVsyncSource()
|
||||||
|
{
|
||||||
|
if (m_Thread != nullptr) {
|
||||||
|
SDL_AtomicSet(&m_Stopping, 1);
|
||||||
|
SDL_WaitThread(m_Thread, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
|
||||||
|
{
|
||||||
|
m_DisplayFps = displayFps;
|
||||||
|
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
|
||||||
|
if (m_Thread == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to create DX V-sync thread: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NullThreadedVsyncSource::vsyncThread(void* context)
|
||||||
|
{
|
||||||
|
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
|
||||||
|
|
||||||
|
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||||
|
|
||||||
|
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
||||||
|
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pacer.h"
|
||||||
|
|
||||||
|
class NullThreadedVsyncSource : public IVsyncSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NullThreadedVsyncSource(Pacer* pacer);
|
||||||
|
|
||||||
|
virtual ~NullThreadedVsyncSource();
|
||||||
|
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int vsyncThread(void* context);
|
||||||
|
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
SDL_Thread* m_Thread;
|
||||||
|
SDL_atomic_t m_Stopping;
|
||||||
|
int m_DisplayFps;
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include "pacer.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
#include "nullthreadedvsyncsource.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include "displaylinkvsyncsource.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#include "dxvsyncsource.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define FRAME_HISTORY_ENTRIES 8
|
||||||
|
|
||||||
|
// We may be woken up slightly late so don't go all the way
|
||||||
|
// up to the next V-sync since we may accidentally step into
|
||||||
|
// the next V-sync period. It also takes some amount of time
|
||||||
|
// to do the render itself, so we can't render right before
|
||||||
|
// V-sync happens.
|
||||||
|
#define TIMER_SLACK_MS 3
|
||||||
|
|
||||||
|
Pacer::Pacer(IFFmpegRenderer* renderer) :
|
||||||
|
m_FrameQueueLock(0),
|
||||||
|
m_VsyncSource(nullptr),
|
||||||
|
m_VsyncRenderer(renderer),
|
||||||
|
m_MaxVideoFps(0),
|
||||||
|
m_DisplayFps(0)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Pacer::~Pacer()
|
||||||
|
{
|
||||||
|
// Stop V-sync callbacks
|
||||||
|
delete m_VsyncSource;
|
||||||
|
m_VsyncSource = nullptr;
|
||||||
|
|
||||||
|
while (!m_FrameQueue.isEmpty()) {
|
||||||
|
AVFrame* frame = m_FrameQueue.dequeue();
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called in an arbitrary thread by the IVsyncSource on V-sync
|
||||||
|
// or an event synchronized with V-sync
|
||||||
|
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
||||||
|
{
|
||||||
|
// Make sure initialize() has been called
|
||||||
|
SDL_assert(m_MaxVideoFps != 0);
|
||||||
|
|
||||||
|
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
|
||||||
|
|
||||||
|
Uint32 vsyncCallbackStartTime = SDL_GetTicks();
|
||||||
|
|
||||||
|
SDL_AtomicLock(&m_FrameQueueLock);
|
||||||
|
|
||||||
|
// If the queue length history entries are large, be strict
|
||||||
|
// about dropping excess frames.
|
||||||
|
int frameDropTarget = 1;
|
||||||
|
|
||||||
|
// If we may get more frames per second than we can display, use
|
||||||
|
// frame history to drop frames only if consistently above the
|
||||||
|
// one queued frame mark.
|
||||||
|
if (m_MaxVideoFps >= m_DisplayFps) {
|
||||||
|
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
|
||||||
|
if (m_FrameQueueHistory[i] <= 1) {
|
||||||
|
// Be lenient as long as the queue length
|
||||||
|
// resolves before the end of frame history
|
||||||
|
frameDropTarget = 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
|
||||||
|
m_FrameQueueHistory.dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch up if we're several frames ahead
|
||||||
|
while (m_FrameQueue.count() > frameDropTarget) {
|
||||||
|
AVFrame* frame = m_FrameQueue.dequeue();
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (m_FrameQueue.isEmpty()) {
|
||||||
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
|
||||||
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(),
|
||||||
|
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
|
||||||
|
SDL_AtomicLock(&m_FrameQueueLock);
|
||||||
|
if (!m_FrameQueue.isEmpty()) {
|
||||||
|
// Don't release the lock
|
||||||
|
goto RenderNextFrame;
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to render at this time
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderNextFrame:
|
||||||
|
// Grab the first frame
|
||||||
|
AVFrame* frame = m_FrameQueue.dequeue();
|
||||||
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
|
||||||
|
// Render it
|
||||||
|
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||||
|
|
||||||
|
// Free the frame
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
||||||
|
{
|
||||||
|
m_MaxVideoFps = maxVideoFps;
|
||||||
|
m_EnableVsync = enableVsync;
|
||||||
|
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||||
|
|
||||||
|
if (m_EnableVsync) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
|
||||||
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
|
|
||||||
|
#if defined(Q_OS_DARWIN)
|
||||||
|
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
||||||
|
#elif defined(Q_OS_WIN32)
|
||||||
|
m_VsyncSource = new DxVsyncSource(this);
|
||||||
|
#else
|
||||||
|
// Platforms without a VsyncSource will just render frames
|
||||||
|
// immediately like they used to.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
|
||||||
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pacer::submitFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
// Make sure initialize() has been called
|
||||||
|
SDL_assert(m_MaxVideoFps != 0);
|
||||||
|
|
||||||
|
// Queue the frame until the V-sync callback if
|
||||||
|
// we have a V-sync source, otherwise deliver it
|
||||||
|
// immediately and hope for the best.
|
||||||
|
if (isUsingFrameQueue()) {
|
||||||
|
SDL_AtomicLock(&m_FrameQueueLock);
|
||||||
|
m_FrameQueue.enqueue(frame);
|
||||||
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Pacer::isUsingFrameQueue()
|
||||||
|
{
|
||||||
|
return m_VsyncSource != nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../renderer.h"
|
||||||
|
|
||||||
|
#include <QQueue>
|
||||||
|
|
||||||
|
class IVsyncSource {
|
||||||
|
public:
|
||||||
|
virtual ~IVsyncSource() {}
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Pacer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Pacer(IFFmpegRenderer* renderer);
|
||||||
|
|
||||||
|
~Pacer();
|
||||||
|
|
||||||
|
void submitFrame(AVFrame* frame);
|
||||||
|
|
||||||
|
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
|
||||||
|
|
||||||
|
void vsyncCallback(int timeUntilNextVsyncMillis);
|
||||||
|
|
||||||
|
bool isUsingFrameQueue();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QQueue<AVFrame*> m_FrameQueue;
|
||||||
|
QQueue<int> m_FrameQueueHistory;
|
||||||
|
SDL_SpinLock m_FrameQueueLock;
|
||||||
|
|
||||||
|
IVsyncSource* m_VsyncSource;
|
||||||
|
IFFmpegRenderer* m_VsyncRenderer;
|
||||||
|
int m_MaxVideoFps;
|
||||||
|
int m_DisplayFps;
|
||||||
|
bool m_EnableVsync;
|
||||||
|
};
|
||||||
@@ -8,13 +8,24 @@ extern "C" {
|
|||||||
|
|
||||||
class IFFmpegRenderer {
|
class IFFmpegRenderer {
|
||||||
public:
|
public:
|
||||||
|
enum VSyncConstraint {
|
||||||
|
VSYNC_FORCE_OFF,
|
||||||
|
VSYNC_FORCE_ON,
|
||||||
|
VSYNC_ANY
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~IFFmpegRenderer() {}
|
virtual ~IFFmpegRenderer() {}
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height) = 0;
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync) = 0;
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
||||||
virtual void renderFrame(AVFrame* frame) = 0;
|
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
||||||
|
virtual bool needsTestFrame() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
|
virtual VSyncConstraint getVsyncConstraint() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdlRenderer : public IFFmpegRenderer {
|
class SdlRenderer : public IFFmpegRenderer {
|
||||||
@@ -24,9 +35,14 @@ public:
|
|||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height);
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync);
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrame(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* m_Renderer;
|
SDL_Renderer* m_Renderer;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "renderer.h"
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
SdlRenderer::SdlRenderer()
|
SdlRenderer::SdlRenderer()
|
||||||
: m_Renderer(nullptr),
|
: m_Renderer(nullptr),
|
||||||
m_Texture(nullptr)
|
m_Texture(nullptr)
|
||||||
@@ -21,15 +23,50 @@ SdlRenderer::~SdlRenderer()
|
|||||||
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
|
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
|
||||||
{
|
{
|
||||||
/* Nothing to do */
|
/* Nothing to do */
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using SDL software renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SdlRenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
// This renderer should always work
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SdlRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
// The FFmpeg CPU decoder can handle reference frame invalidation,
|
||||||
|
// but only for H.264.
|
||||||
|
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
int,
|
int,
|
||||||
int width,
|
int width,
|
||||||
int height)
|
int height,
|
||||||
|
int,
|
||||||
|
bool enableVsync)
|
||||||
{
|
{
|
||||||
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
|
||||||
|
|
||||||
|
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
|
||||||
|
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
|
||||||
|
// configuration but doesn't seem feasible to detect here.
|
||||||
|
if (enableVsync) {
|
||||||
|
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
|
||||||
if (!m_Renderer) {
|
if (!m_Renderer) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateRenderer() failed: %s",
|
"SDL_CreateRenderer() failed: %s",
|
||||||
@@ -41,6 +78,11 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
|||||||
// logical rendering surface size is equal to the stream size
|
// logical rendering surface size is equal to the stream size
|
||||||
SDL_RenderSetLogicalSize(m_Renderer, width, height);
|
SDL_RenderSetLogicalSize(m_Renderer, width, height);
|
||||||
|
|
||||||
|
// Draw a black frame until the video stream starts rendering
|
||||||
|
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||||
|
SDL_RenderClear(m_Renderer);
|
||||||
|
SDL_RenderPresent(m_Renderer);
|
||||||
|
|
||||||
m_Texture = SDL_CreateTexture(m_Renderer,
|
m_Texture = SDL_CreateTexture(m_Renderer,
|
||||||
SDL_PIXELFORMAT_YV12,
|
SDL_PIXELFORMAT_YV12,
|
||||||
SDL_TEXTUREACCESS_STREAMING,
|
SDL_TEXTUREACCESS_STREAMING,
|
||||||
@@ -56,7 +98,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlRenderer::renderFrame(AVFrame* frame)
|
void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
{
|
{
|
||||||
SDL_UpdateYUVTexture(m_Texture, nullptr,
|
SDL_UpdateYUVTexture(m_Texture, nullptr,
|
||||||
frame->data[0],
|
frame->data[0],
|
||||||
@@ -65,6 +107,7 @@ void SdlRenderer::renderFrame(AVFrame* frame)
|
|||||||
frame->linesize[1],
|
frame->linesize[1],
|
||||||
frame->data[2],
|
frame->data[2],
|
||||||
frame->linesize[2]);
|
frame->linesize[2]);
|
||||||
|
|
||||||
SDL_RenderClear(m_Renderer);
|
SDL_RenderClear(m_Renderer);
|
||||||
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
||||||
SDL_RenderPresent(m_Renderer);
|
SDL_RenderPresent(m_Renderer);
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#include "vaapi.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
VAAPIRenderer::VAAPIRenderer()
|
||||||
|
: m_HwContext(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
VAAPIRenderer::~VAAPIRenderer()
|
||||||
|
{
|
||||||
|
if (m_HwContext != nullptr) {
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
// Hold onto this VADisplay since we'll need it to uninitialize VAAPI
|
||||||
|
VADisplay display = vaDeviceContext->display;
|
||||||
|
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
|
||||||
|
if (display) {
|
||||||
|
vaTerminate(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
m_VideoWidth = width;
|
||||||
|
m_VideoHeight = height;
|
||||||
|
|
||||||
|
SDL_GetWindowSize(window, &m_DisplayWidth, &m_DisplayHeight);
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
|
||||||
|
if (!m_HwContext) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to allocate VAAPI context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
m_WindowSystem = info.subsystem;
|
||||||
|
if (info.subsystem == SDL_SYSWM_X11) {
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
m_XWindow = info.info.x11.window;
|
||||||
|
vaDeviceContext->display = vaGetDisplay(info.info.x11.display);
|
||||||
|
if (!vaDeviceContext->display) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open X11 display for VAAPI");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Moonlight not compiled with VAAPI X11 support!");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
m_WaylandSurface = info.info.wl.surface;
|
||||||
|
m_WaylandDisplay = info.info.wl.display;
|
||||||
|
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
|
||||||
|
if (!vaDeviceContext->display) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open Wayland display for VAAPI");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Moonlight not compiled with VAAPI Wayland support!");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unsupported VAAPI rendering subsystem: %d",
|
||||||
|
info.subsystem);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int major, minor;
|
||||||
|
VAStatus status;
|
||||||
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to initialize VAAPI: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Initialized VAAPI %d.%d",
|
||||||
|
major, minor);
|
||||||
|
|
||||||
|
// This will populate the driver_quirks
|
||||||
|
err = av_hwdevice_ctx_init(m_HwContext);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to initialize VAAPI context: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This quirk is set for the VDPAU wrapper which doesn't work with our VAAPI renderer
|
||||||
|
if (vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES) {
|
||||||
|
// Fail and let our VDPAU renderer pick this up
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Avoiding VDPAU wrapper for VAAPI decoding");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
|
||||||
|
{
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VAAPI accelerated renderer on %s",
|
||||||
|
m_WindowSystem == SDL_SYSWM_X11 ? "X11" : "Wayland");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
// We need a test frame to see if this VAAPI driver
|
||||||
|
// supports the profile used for streaming
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
VAAPIRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
|
{
|
||||||
|
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
if (m_WindowSystem == SDL_SYSWM_X11) {
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
vaPutSurface(vaDeviceContext->display,
|
||||||
|
surface,
|
||||||
|
m_XWindow,
|
||||||
|
0, 0,
|
||||||
|
m_VideoWidth, m_VideoHeight,
|
||||||
|
dst.x, dst.y,
|
||||||
|
dst.w, dst.h,
|
||||||
|
NULL, 0, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else if (m_WindowSystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
struct wl_buffer* buffer;
|
||||||
|
VAStatus status;
|
||||||
|
|
||||||
|
status = vaGetSurfaceBufferWl(vaDeviceContext->display,
|
||||||
|
surface,
|
||||||
|
VA_FRAME_PICTURE,
|
||||||
|
&buffer);
|
||||||
|
if (status == VA_STATUS_SUCCESS) {
|
||||||
|
wl_surface_attach(m_WaylandSurface, buffer, 0, 0);
|
||||||
|
wl_surface_damage(m_WaylandSurface, dst.x, dst.y, dst.w, dst.h);
|
||||||
|
|
||||||
|
wl_display_flush(m_WaylandDisplay);
|
||||||
|
wl_surface_commit(m_WaylandSurface);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaGetSurfaceBufferWl failed(): %d",
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We don't accept anything else in initialize().
|
||||||
|
SDL_assert(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
// Avoid X11 if SDL was built without it
|
||||||
|
#ifndef SDL_VIDEO_DRIVER_X11
|
||||||
|
#warning Unable to use libva-x11 without SDL support
|
||||||
|
#undef HAVE_LIBVA_X11
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Avoid Wayland if SDL was built without it
|
||||||
|
#ifndef SDL_VIDEO_DRIVER_WAYLAND
|
||||||
|
#warning Unable to use libva-wayland without SDL support
|
||||||
|
#undef HAVE_LIBVA_WAYLAND
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <va/va.h>
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
#include <va/va_x11.h>
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
#include <va/va_wayland.h>
|
||||||
|
#endif
|
||||||
|
#include <libavutil/hwcontext_vaapi.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VAAPIRenderer : public IFFmpegRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VAAPIRenderer();
|
||||||
|
virtual ~VAAPIRenderer();
|
||||||
|
virtual bool initialize(SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync);
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_WindowSystem;
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBVA_X11
|
||||||
|
Window m_XWindow;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBVA_WAYLAND
|
||||||
|
struct wl_surface* m_WaylandSurface;
|
||||||
|
struct wl_display* m_WaylandDisplay;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int m_VideoWidth;
|
||||||
|
int m_VideoHeight;
|
||||||
|
int m_DisplayWidth;
|
||||||
|
int m_DisplayHeight;
|
||||||
|
};
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
#include "vdpau.h"
|
||||||
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#define BAIL_ON_FAIL(status, something) if ((status) != VDP_STATUS_OK) { \
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, \
|
||||||
|
#something " failed: %d", (status)); \
|
||||||
|
return false; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define GET_PROC_ADDRESS(id, func) status = vdpauCtx->get_proc_address(m_Device, id, (void**)func); \
|
||||||
|
BAIL_ON_FAIL(status, id)
|
||||||
|
|
||||||
|
const VdpRGBAFormat VDPAURenderer::k_OutputFormats[] = {
|
||||||
|
VDP_RGBA_FORMAT_B8G8R8A8,
|
||||||
|
VDP_RGBA_FORMAT_R8G8B8A8
|
||||||
|
};
|
||||||
|
|
||||||
|
VDPAURenderer::VDPAURenderer()
|
||||||
|
: m_HwContext(nullptr),
|
||||||
|
m_PresentationQueueTarget(0),
|
||||||
|
m_PresentationQueue(0),
|
||||||
|
m_VideoMixer(0),
|
||||||
|
m_NextSurfaceIndex(0)
|
||||||
|
{
|
||||||
|
SDL_zero(m_OutputSurface);
|
||||||
|
}
|
||||||
|
|
||||||
|
VDPAURenderer::~VDPAURenderer()
|
||||||
|
{
|
||||||
|
if (m_PresentationQueue != 0) {
|
||||||
|
m_VdpPresentationQueueDestroy(m_PresentationQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_VideoMixer != 0) {
|
||||||
|
m_VdpVideoMixerDestroy(m_VideoMixer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_PresentationQueueTarget != 0) {
|
||||||
|
m_VdpPresentationQueueTargetDestroy(m_PresentationQueueTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
|
||||||
|
if (m_OutputSurface[i] != 0) {
|
||||||
|
m_VdpOutputSurfaceDestroy(m_OutputSurface[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must be done last as it frees VDPAU context required to call
|
||||||
|
// the functions above.
|
||||||
|
if (m_HwContext != nullptr) {
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
VdpStatus status;
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
m_VideoWidth = width;
|
||||||
|
m_VideoHeight = height;
|
||||||
|
|
||||||
|
err = av_hwdevice_ctx_create(&m_HwContext,
|
||||||
|
AV_HWDEVICE_TYPE_VDPAU,
|
||||||
|
nullptr, nullptr, 0);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create VDPAU context: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* devCtx = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVDPAUDeviceContext* vdpauCtx = (AVVDPAUDeviceContext*)devCtx->hwctx;
|
||||||
|
m_Device = vdpauCtx->device;
|
||||||
|
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_ERROR_STRING, &m_VdpGetErrorString);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_DESTROY, &m_VdpPresentationQueueTargetDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_CREATE, &m_VdpVideoMixerCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_DESTROY, &m_VdpVideoMixerDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_MIXER_RENDER, &m_VdpVideoMixerRender);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_CREATE, &m_VdpPresentationQueueCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DESTROY, &m_VdpPresentationQueueDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_DISPLAY, &m_VdpPresentationQueueDisplay);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_SET_BACKGROUND_COLOR, &m_VdpPresentationQueueSetBackgroundColor);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_BLOCK_UNTIL_SURFACE_IDLE, &m_VdpPresentationQueueBlockUntilSurfaceIdle);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_CREATE, &m_VdpOutputSurfaceCreate);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
|
||||||
|
|
||||||
|
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(info.subsystem == SDL_SYSWM_X11);
|
||||||
|
|
||||||
|
if (info.subsystem == SDL_SYSWM_X11) {
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_PRESENTATION_QUEUE_TARGET_CREATE_X11,
|
||||||
|
&m_VdpPresentationQueueTargetCreateX11);
|
||||||
|
status = m_VdpPresentationQueueTargetCreateX11(m_Device,
|
||||||
|
info.info.x11.window,
|
||||||
|
&m_PresentationQueueTarget);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueTargetCreateX11() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VDPAU backend does not currently support Wayland");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unsupported VDPAU rendering subsystem: %d",
|
||||||
|
info.subsystem);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try our available output formats to find something the GPU supports
|
||||||
|
bool foundFormat = false;
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
|
||||||
|
VdpBool supported;
|
||||||
|
uint32_t maxWidth, maxHeight;
|
||||||
|
status = m_VdpOutputSurfaceQueryCapabilities(m_Device, k_OutputFormats[i],
|
||||||
|
&supported, &maxWidth, &maxHeight);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceQueryCapabilities() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supported) {
|
||||||
|
if (m_DisplayWidth <= maxWidth && m_DisplayHeight <= maxHeight) {
|
||||||
|
m_OutputSurfaceFormat = k_OutputFormats[i];
|
||||||
|
foundFormat = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Display size not within capabilities %dx%d vs %dx%d",
|
||||||
|
m_DisplayWidth, m_DisplayWidth,
|
||||||
|
maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundFormat) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No compatible output surface format found!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the output surfaces
|
||||||
|
for (int i = 0; i < OUTPUT_SURFACE_COUNT; i++) {
|
||||||
|
// It seems there's some lazy freeing going on or something in VDPAU
|
||||||
|
// because we can get VDP_STATUS_RESOURCES, then wait a bit and it'll
|
||||||
|
// complete without a problem.
|
||||||
|
int tries = 1;
|
||||||
|
do {
|
||||||
|
status = m_VdpOutputSurfaceCreate(m_Device, m_OutputSurfaceFormat,
|
||||||
|
m_DisplayWidth, m_DisplayHeight,
|
||||||
|
&m_OutputSurface[i]);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceCreate() try #%d: %s",
|
||||||
|
tries,
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
SDL_Delay(250);
|
||||||
|
}
|
||||||
|
} while (status == VDP_STATUS_RESOURCES && ++tries <= 10);
|
||||||
|
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpOutputSurfaceCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status = m_VdpPresentationQueueCreate(m_Device, m_PresentationQueueTarget,
|
||||||
|
&m_PresentationQueue);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the background to opaque black
|
||||||
|
VdpColor color = {0.0, 0.0, 0.0, 1.0};
|
||||||
|
m_VdpPresentationQueueSetBackgroundColor(m_PresentationQueue, &color);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
|
||||||
|
{
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
// Allow HEVC usage on VDPAU. This was disabled by FFmpeg due to
|
||||||
|
// GL interop issues, but we use VDPAU for rendering so it's no issue.
|
||||||
|
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
||||||
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
||||||
|
|
||||||
|
// This flag is recommended due to hardware underreporting supported levels
|
||||||
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VDPAU accelerated renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VDPAURenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
// We need a test frame to see if this VDPAU driver
|
||||||
|
// supports the profile used for streaming
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int VDPAURenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
|
{
|
||||||
|
VdpStatus status;
|
||||||
|
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
||||||
|
|
||||||
|
// This is safe without locking because this is always called on the main thread
|
||||||
|
VdpOutputSurface chosenSurface = m_OutputSurface[m_NextSurfaceIndex];
|
||||||
|
m_NextSurfaceIndex = (m_NextSurfaceIndex + 1) % OUTPUT_SURFACE_COUNT;
|
||||||
|
|
||||||
|
// We need to create the mixer on the fly, because we don't know the dimensions
|
||||||
|
// of our video surfaces in advance of decoding
|
||||||
|
if (m_VideoMixer == 0) {
|
||||||
|
VdpChromaType videoSurfaceChroma;
|
||||||
|
uint32_t videoSurfaceWidth, videoSurfaceHeight;
|
||||||
|
status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma,
|
||||||
|
&videoSurfaceWidth, &videoSurfaceHeight);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoSurfaceGetParameters() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VDPAU surface size: %dx%d",
|
||||||
|
videoSurfaceWidth, videoSurfaceHeight);
|
||||||
|
|
||||||
|
#define PARAM_COUNT 3
|
||||||
|
const VdpVideoMixerParameter params[PARAM_COUNT] = {
|
||||||
|
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
|
||||||
|
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
|
||||||
|
VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
|
||||||
|
};
|
||||||
|
const void* const paramValues[PARAM_COUNT] = {
|
||||||
|
&videoSurfaceWidth,
|
||||||
|
&videoSurfaceHeight,
|
||||||
|
&videoSurfaceChroma,
|
||||||
|
};
|
||||||
|
|
||||||
|
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
|
||||||
|
PARAM_COUNT, params, paramValues,
|
||||||
|
&m_VideoMixer);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoMixerCreate() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for this frame to be off the screen
|
||||||
|
VdpTime pts;
|
||||||
|
m_VdpPresentationQueueBlockUntilSurfaceIdle(m_PresentationQueue, chosenSurface, &pts);
|
||||||
|
|
||||||
|
VdpRect sourceRect, outputRect;
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_DisplayWidth;
|
||||||
|
dst.h = m_DisplayHeight;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
outputRect.x0 = dst.x;
|
||||||
|
outputRect.x1 = dst.x + dst.w;
|
||||||
|
outputRect.y0 = dst.y;
|
||||||
|
outputRect.y1 = dst.y + dst.h;
|
||||||
|
|
||||||
|
sourceRect.x0 = sourceRect.y0 = 0;
|
||||||
|
sourceRect.x1 = m_VideoWidth;
|
||||||
|
sourceRect.y1 = m_VideoHeight;
|
||||||
|
|
||||||
|
// Render the next frame into the output surface
|
||||||
|
status = m_VdpVideoMixerRender(m_VideoMixer,
|
||||||
|
VDP_INVALID_HANDLE, nullptr,
|
||||||
|
VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME,
|
||||||
|
0, nullptr,
|
||||||
|
videoSurface,
|
||||||
|
0, nullptr,
|
||||||
|
&sourceRect,
|
||||||
|
chosenSurface,
|
||||||
|
&outputRect,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
nullptr);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpVideoMixerRender() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue the frame for display immediately
|
||||||
|
status = m_VdpPresentationQueueDisplay(m_PresentationQueue, chosenSurface, 0, 0, 0);
|
||||||
|
if (status != VDP_STATUS_OK) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VdpPresentationQueueDisplay() failed: %s",
|
||||||
|
m_VdpGetErrorString(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <vdpau/vdpau.h>
|
||||||
|
#include <vdpau/vdpau_x11.h>
|
||||||
|
#include <libavutil/hwcontext_vdpau.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VDPAURenderer : public IFFmpegRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VDPAURenderer();
|
||||||
|
virtual ~VDPAURenderer();
|
||||||
|
virtual bool initialize(SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync);
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t m_VideoWidth, m_VideoHeight;
|
||||||
|
uint32_t m_DisplayWidth, m_DisplayHeight;
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
|
VdpPresentationQueueTarget m_PresentationQueueTarget;
|
||||||
|
VdpPresentationQueue m_PresentationQueue;
|
||||||
|
VdpVideoMixer m_VideoMixer;
|
||||||
|
VdpRGBAFormat m_OutputSurfaceFormat;
|
||||||
|
VdpDevice m_Device;
|
||||||
|
|
||||||
|
#define OUTPUT_SURFACE_COUNT 3
|
||||||
|
VdpOutputSurface m_OutputSurface[OUTPUT_SURFACE_COUNT];
|
||||||
|
int m_NextSurfaceIndex;
|
||||||
|
|
||||||
|
#define OUTPUT_SURFACE_FORMAT_COUNT 2
|
||||||
|
static const VdpRGBAFormat k_OutputFormats[OUTPUT_SURFACE_FORMAT_COUNT];
|
||||||
|
|
||||||
|
VdpGetErrorString* m_VdpGetErrorString;
|
||||||
|
VdpPresentationQueueTargetDestroy* m_VdpPresentationQueueTargetDestroy;
|
||||||
|
VdpVideoMixerCreate* m_VdpVideoMixerCreate;
|
||||||
|
VdpVideoMixerDestroy* m_VdpVideoMixerDestroy;
|
||||||
|
VdpVideoMixerRender* m_VdpVideoMixerRender;
|
||||||
|
VdpPresentationQueueCreate* m_VdpPresentationQueueCreate;
|
||||||
|
VdpPresentationQueueDestroy* m_VdpPresentationQueueDestroy;
|
||||||
|
VdpPresentationQueueDisplay* m_VdpPresentationQueueDisplay;
|
||||||
|
VdpPresentationQueueSetBackgroundColor* m_VdpPresentationQueueSetBackgroundColor;
|
||||||
|
VdpPresentationQueueBlockUntilSurfaceIdle* m_VdpPresentationQueueBlockUntilSurfaceIdle;
|
||||||
|
VdpOutputSurfaceCreate* m_VdpOutputSurfaceCreate;
|
||||||
|
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
|
||||||
|
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
|
||||||
|
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
|
||||||
|
|
||||||
|
// X11 stuff
|
||||||
|
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
// libavutil both defining AVMediaType
|
// libavutil both defining AVMediaType
|
||||||
#define AVMediaType AVMediaType_FFmpeg
|
#define AVMediaType AVMediaType_FFmpeg
|
||||||
#include "vt.h"
|
#include "vt.h"
|
||||||
|
#include "pacer/pacer.h"
|
||||||
#undef AVMediaType
|
#undef AVMediaType
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
#include <mach/mach_time.h>
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import <VideoToolbox/VideoToolbox.h>
|
#import <VideoToolbox/VideoToolbox.h>
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
@@ -20,7 +22,6 @@ public:
|
|||||||
m_FormatDesc(nullptr),
|
m_FormatDesc(nullptr),
|
||||||
m_View(nullptr)
|
m_View(nullptr)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~VTRenderer()
|
virtual ~VTRenderer()
|
||||||
@@ -32,13 +33,76 @@ public:
|
|||||||
if (m_FormatDesc != nullptr) {
|
if (m_FormatDesc != nullptr) {
|
||||||
CFRelease(m_FormatDesc);
|
CFRelease(m_FormatDesc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_View != nullptr) {
|
||||||
|
[m_View removeFromSuperview];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Caller frees frame after we return
|
||||||
|
virtual void renderFrameAtVsync(AVFrame* frame) override
|
||||||
|
{
|
||||||
|
OSStatus status;
|
||||||
|
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
||||||
|
|
||||||
|
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Resetting failed AVSampleBufferDisplay layer");
|
||||||
|
|
||||||
|
// Trigger the main thread to recreate the decoder
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the format has changed or doesn't exist yet, construct it with the
|
||||||
|
// pixel buffer data
|
||||||
|
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
|
||||||
|
if (m_FormatDesc != nullptr) {
|
||||||
|
CFRelease(m_FormatDesc);
|
||||||
|
}
|
||||||
|
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
|
||||||
|
pixBuf, &m_FormatDesc);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
|
||||||
|
status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue this sample for the next v-sync
|
||||||
|
CMSampleTimingInfo timingInfo = {
|
||||||
|
.duration = kCMTimeInvalid,
|
||||||
|
.decodeTimeStamp = kCMTimeInvalid,
|
||||||
|
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
CMSampleBufferRef sampleBuffer;
|
||||||
|
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
|
||||||
|
pixBuf,
|
||||||
|
m_FormatDesc,
|
||||||
|
&timingInfo,
|
||||||
|
&sampleBuffer);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
|
||||||
|
status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
|
||||||
|
|
||||||
|
CFRelease(sampleBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
int,
|
int,
|
||||||
int) override
|
int,
|
||||||
|
int,
|
||||||
|
bool) override
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
@@ -119,54 +183,30 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context) override
|
virtual bool prepareDecoderContext(AVCodecContext* context) override
|
||||||
{
|
{
|
||||||
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using VideoToolbox accelerated renderer");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void renderFrame(AVFrame* frame) override
|
virtual bool needsTestFrame() override
|
||||||
{
|
{
|
||||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
// We query VT to determine whether the codec is supported
|
||||||
OSStatus status;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
virtual int getDecoderCapabilities() override
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
{
|
||||||
"Resetting failed AVSampleBufferDisplay layer");
|
return 0;
|
||||||
setupDisplayLayer();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the format has changed or doesn't exist yet, construct it with the
|
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
|
||||||
// pixel buffer data
|
{
|
||||||
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
|
// This renderer is inherently tied to V-sync due how we're
|
||||||
status = CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault,
|
// rendering with AVSampleBufferDisplay layer. Running without
|
||||||
pixBuf, &m_FormatDesc);
|
// the V-Sync source leads to massive stuttering.
|
||||||
if (status != noErr) {
|
return VSYNC_FORCE_ON;
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
|
|
||||||
status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
|
||||||
status = CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
|
|
||||||
pixBuf,
|
|
||||||
m_FormatDesc,
|
|
||||||
&kCMTimingInfoInvalid,
|
|
||||||
&sampleBuffer);
|
|
||||||
if (status != noErr) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
|
|
||||||
status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
|
|
||||||
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
|
|
||||||
|
|
||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
|
|
||||||
|
|
||||||
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
|
|
||||||
|
|
||||||
CFRelease(sampleBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1,94 +1,69 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#include "ffmpeg-renderers/dxva2.h"
|
#include "ffmpeg-renderers/dxva2.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::chooseDecoder(
|
#ifdef Q_OS_DARWIN
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
#include "ffmpeg-renderers/vt.h"
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int width, int height,
|
|
||||||
AVCodec*& chosenDecoder,
|
|
||||||
const AVCodecHWConfig*& chosenHwConfig,
|
|
||||||
IFFmpegRenderer*& newRenderer)
|
|
||||||
{
|
|
||||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
|
||||||
}
|
|
||||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
|
||||||
chosenDecoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Q_ASSERT(false);
|
|
||||||
chosenDecoder = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chosenDecoder) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to find decoder for format: %x",
|
|
||||||
videoFormat);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0;; i++) {
|
|
||||||
const AVCodecHWConfig *config = avcodec_get_hw_config(chosenDecoder, i);
|
|
||||||
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
|
||||||
// No matching hardware acceleration support.
|
|
||||||
// This is not an error.
|
|
||||||
chosenHwConfig = nullptr;
|
|
||||||
newRenderer = new SdlRenderer();
|
|
||||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
|
||||||
newRenderer->initialize(window, videoFormat, width, height)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete newRenderer;
|
|
||||||
newRenderer = nullptr;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for acceleration types we support
|
|
||||||
switch (config->device_type) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
case AV_HWDEVICE_TYPE_DXVA2:
|
|
||||||
newRenderer = new DXVA2Renderer();
|
|
||||||
break;
|
|
||||||
#endif
|
#endif
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newRenderer->initialize(window, videoFormat, width, height)) {
|
#ifdef HAVE_LIBVA
|
||||||
chosenHwConfig = config;
|
#include "ffmpeg-renderers/vaapi.h"
|
||||||
return true;
|
#endif
|
||||||
}
|
|
||||||
else {
|
#ifdef HAVE_LIBVDPAU
|
||||||
// Failed to initialize
|
#include "ffmpeg-renderers/vdpau.h"
|
||||||
delete newRenderer;
|
#endif
|
||||||
newRenderer = nullptr;
|
|
||||||
}
|
// This is gross but it allows us to use sizeof()
|
||||||
}
|
#include "ffmpeg_videosamples.cpp"
|
||||||
}
|
|
||||||
|
#define FAILED_DECODES_RESET_THRESHOLD 20
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||||
{
|
{
|
||||||
return m_HwDecodeCfg != nullptr;
|
return m_HwDecodeCfg != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return m_Renderer->getDecoderCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||||
|
const enum AVPixelFormat* pixFmts)
|
||||||
|
{
|
||||||
|
FFmpegVideoDecoder* decoder = (FFmpegVideoDecoder*)context->opaque;
|
||||||
|
const enum AVPixelFormat *p;
|
||||||
|
|
||||||
|
for (p = pixFmts; *p != -1; p++) {
|
||||||
|
// Only match our hardware decoding codec or SW pixel
|
||||||
|
// format (if not using hardware decoding). It's crucial
|
||||||
|
// to override the default get_format() which will try
|
||||||
|
// to gracefully fall back to software decode and break us.
|
||||||
|
if (*p == (decoder->m_HwDecodeCfg ?
|
||||||
|
decoder->m_HwDecodeCfg->pix_fmt :
|
||||||
|
context->pix_fmt)) {
|
||||||
|
return *p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AV_PIX_FMT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
FFmpegVideoDecoder::FFmpegVideoDecoder()
|
FFmpegVideoDecoder::FFmpegVideoDecoder()
|
||||||
: m_VideoDecoderCtx(nullptr),
|
: m_VideoDecoderCtx(nullptr),
|
||||||
m_DecodeBuffer(1024 * 1024, 0),
|
m_DecodeBuffer(1024 * 1024, 0),
|
||||||
m_HwDecodeCfg(nullptr),
|
m_HwDecodeCfg(nullptr),
|
||||||
m_Renderer(nullptr)
|
m_Renderer(nullptr),
|
||||||
|
m_ConsecutiveFailedDecodes(0),
|
||||||
|
m_Pacer(nullptr)
|
||||||
{
|
{
|
||||||
av_init_packet(&m_Pkt);
|
av_init_packet(&m_Pkt);
|
||||||
|
SDL_AtomicSet(&m_QueuedFrames, 0);
|
||||||
|
|
||||||
// Use linear filtering when renderer scaling is required
|
// Use linear filtering when renderer scaling is required
|
||||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
||||||
@@ -96,29 +71,74 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
|
|
||||||
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
||||||
{
|
{
|
||||||
avcodec_close(m_VideoDecoderCtx);
|
reset();
|
||||||
av_free(m_VideoDecoderCtx);
|
}
|
||||||
m_VideoDecoderCtx = nullptr;
|
|
||||||
|
|
||||||
m_HwDecodeCfg = nullptr;
|
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
|
||||||
|
{
|
||||||
|
return m_Renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::reset()
|
||||||
|
{
|
||||||
|
// Drop any frames still queued to ensure
|
||||||
|
// they are properly freed.
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
while (SDL_AtomicGet(&m_QueuedFrames) > 0) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Waiting for %d frames to return",
|
||||||
|
SDL_AtomicGet(&m_QueuedFrames));
|
||||||
|
|
||||||
|
if (SDL_PeepEvents(&event,
|
||||||
|
1,
|
||||||
|
SDL_GETEVENT,
|
||||||
|
SDL_USEREVENT,
|
||||||
|
SDL_USEREVENT) == 1) {
|
||||||
|
dropFrame(&event.user);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_Delay(10);
|
||||||
|
SDL_PumpEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete m_Pacer;
|
||||||
|
m_Pacer = nullptr;
|
||||||
|
|
||||||
|
// This must be called after deleting Pacer because it
|
||||||
|
// may be holding AVFrames to free in its destructor.
|
||||||
|
// However, it must be called before deleting the IFFmpegRenderer
|
||||||
|
// since the codec context may be referencing objects that we
|
||||||
|
// need to delete in the renderer destructor.
|
||||||
|
avcodec_free_context(&m_VideoDecoderCtx);
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::initialize(
|
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
int videoFormat, int width, int height,
|
||||||
SDL_Window* window,
|
int maxFps, bool enableVsync, bool testOnly)
|
||||||
int videoFormat,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
int)
|
|
||||||
{
|
{
|
||||||
AVCodec* decoder;
|
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
|
||||||
|
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync is forcefully disabled by the active renderer");
|
||||||
|
enableVsync = false;
|
||||||
|
}
|
||||||
|
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
|
||||||
|
// FIXME: This duplicates logic in Session.cpp
|
||||||
|
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
||||||
|
if (displayHz + 5 >= maxFps) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync is forcefully enabled by the active renderer");
|
||||||
|
enableVsync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height,
|
m_Pacer = new Pacer(m_Renderer);
|
||||||
decoder, m_HwDecodeCfg, m_Renderer)) {
|
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
||||||
// Error logged in chooseDecoder()
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +152,10 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
// Always request low delay decoding
|
// Always request low delay decoding
|
||||||
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||||
|
|
||||||
|
// Allow display of corrupt frames and frames missing references
|
||||||
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
|
||||||
|
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
|
||||||
|
|
||||||
// Enable slice multi-threading for software decoding
|
// Enable slice multi-threading for software decoding
|
||||||
if (!m_HwDecodeCfg) {
|
if (!m_HwDecodeCfg) {
|
||||||
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
||||||
@@ -146,12 +170,20 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_VideoDecoderCtx->width = width;
|
m_VideoDecoderCtx->width = width;
|
||||||
m_VideoDecoderCtx->height = height;
|
m_VideoDecoderCtx->height = height;
|
||||||
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
|
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
|
||||||
|
m_VideoDecoderCtx->get_format = ffGetFormat;
|
||||||
|
|
||||||
// Allow the renderer to attach data to this decoder
|
// Allow the renderer to attach data to this decoder
|
||||||
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
|
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nobody must override our ffGetFormat
|
||||||
|
SDL_assert(m_VideoDecoderCtx->get_format == ffGetFormat);
|
||||||
|
|
||||||
|
// Stash a pointer to this object in the context
|
||||||
|
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
|
||||||
|
m_VideoDecoderCtx->opaque = this;
|
||||||
|
|
||||||
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
|
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -160,9 +192,155 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FFMpeg doesn't completely initialize the codec until the codec
|
||||||
|
// config data comes in. This would be too late for us to change
|
||||||
|
// our minds on the selected video codec, so we'll do a trial run
|
||||||
|
// now to see if things will actually work when the video stream
|
||||||
|
// comes in.
|
||||||
|
if (testOnly) {
|
||||||
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
|
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
||||||
|
m_Pkt.size = sizeof(k_H264TestFrame);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_Pkt.data = (uint8_t*)k_HEVCTestFrame;
|
||||||
|
m_Pkt.size = sizeof(k_HEVCTestFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||||
|
if (err < 0) {
|
||||||
|
char errorstring[512];
|
||||||
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Test decode failed: %s", errorstring);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// Restore default log level before streaming
|
||||||
|
av_log_set_level(AV_LOG_INFO);
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
|
||||||
|
{
|
||||||
|
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for acceleration types we support
|
||||||
|
switch (hwDecodeCfg->device_type) {
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
case AV_HWDEVICE_TYPE_DXVA2:
|
||||||
|
return new DXVA2Renderer();
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
|
||||||
|
return VTRendererFactory::createRenderer();
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVA
|
||||||
|
case AV_HWDEVICE_TYPE_VAAPI:
|
||||||
|
return new VAAPIRenderer();
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_LIBVDPAU
|
||||||
|
case AV_HWDEVICE_TYPE_VDPAU:
|
||||||
|
return new VDPAURenderer();
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FFmpegVideoDecoder::initialize(
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int maxFps,
|
||||||
|
bool enableVsync)
|
||||||
|
{
|
||||||
|
AVCodec* decoder;
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// Increase log level during initialization
|
||||||
|
av_log_set_level(AV_LOG_DEBUG);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
|
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
|
}
|
||||||
|
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
|
decoder = avcodec_find_decoder(AV_CODEC_ID_HEVC);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_ASSERT(false);
|
||||||
|
decoder = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoder) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to find decoder for format: %x",
|
||||||
|
videoFormat);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
|
||||||
|
if (!config || vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
|
// No matching hardware acceleration support.
|
||||||
|
// This is not an error.
|
||||||
|
m_HwDecodeCfg = nullptr;
|
||||||
|
m_Renderer = new SdlRenderer();
|
||||||
|
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
|
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Renderer = createAcceleratedRenderer(config);
|
||||||
|
if (!m_Renderer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_HwDecodeCfg = config;
|
||||||
|
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||||
|
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
|
||||||
|
if (m_Renderer->needsTestFrame()) {
|
||||||
|
// The test worked, so now let's initialize it for real
|
||||||
|
reset();
|
||||||
|
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||||
|
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Decoder failed to initialize after successful test");
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No test required. Good to go now.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Failed to initialize or test frame failed, so keep looking
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||||
{
|
{
|
||||||
PLENTRY entry = du->bufferList;
|
PLENTRY entry = du->bufferList;
|
||||||
@@ -188,12 +366,23 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
|
|
||||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Decoding failed: %d", err);
|
|
||||||
char errorstring[512];
|
char errorstring[512];
|
||||||
av_strerror(err, errorstring, sizeof(errorstring));
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Decoding failed: %s", errorstring);
|
"avcodec_send_packet() failed: %s", errorstring);
|
||||||
|
|
||||||
|
// If we've failed a bunch of decodes in a row, the decoder/renderer is
|
||||||
|
// clearly unhealthy, so let's generate a synthetic reset event to trigger
|
||||||
|
// the event loop to destroy and recreate the decoder.
|
||||||
|
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Resetting decoder due to consistent failure");
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_DEVICE_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
|
|
||||||
return DR_NEED_IDR;
|
return DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,17 +397,29 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
|
|
||||||
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
|
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
SDL_Event event;
|
// Reset failed decodes count if we reached this far
|
||||||
|
m_ConsecutiveFailedDecodes = 0;
|
||||||
|
|
||||||
event.type = SDL_USEREVENT;
|
// Queue the frame for rendering from the main thread
|
||||||
event.user.code = SDL_CODE_FRAME_READY;
|
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||||
event.user.data1 = frame;
|
queueFrame(frame);
|
||||||
|
|
||||||
// The main thread will handle freeing this
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
|
||||||
|
char errorstring[512];
|
||||||
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"avcodec_receive_frame() failed: %s", errorstring);
|
||||||
|
|
||||||
|
if (++m_ConsecutiveFailedDecodes == FAILED_DECODES_RESET_THRESHOLD) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Resetting decoder due to consistent failure");
|
||||||
|
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_DEVICE_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
@@ -228,13 +429,23 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||||
m_Renderer->renderFrame(frame);
|
m_Pacer->submitFrame(frame);
|
||||||
av_frame_free(&frame);
|
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called on main thread
|
// Called on main thread
|
||||||
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
||||||
{
|
{
|
||||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||||
av_frame_free(&frame);
|
// If Pacer is using a frame queue, we can just queue the
|
||||||
|
// frame and let it decide whether to drop or not. If Pacer
|
||||||
|
// is submitting directly for rendering, we drop the frame
|
||||||
|
// ourselves.
|
||||||
|
if (m_Pacer->isUsingFrameQueue()) {
|
||||||
|
m_Pacer->submitFrame(frame);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
av_frame_free(&frame);
|
||||||
|
}
|
||||||
|
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "ffmpeg-renderers/renderer.h"
|
#include "ffmpeg-renderers/renderer.h"
|
||||||
|
#include "ffmpeg-renderers/pacer/pacer.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
@@ -16,26 +17,38 @@ public:
|
|||||||
int videoFormat,
|
int videoFormat,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int frameRate) override;
|
int maxFps,
|
||||||
|
bool enableVsync) override;
|
||||||
virtual bool isHardwareAccelerated() override;
|
virtual bool isHardwareAccelerated() override;
|
||||||
|
virtual int getDecoderCapabilities() override;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) override;
|
virtual void renderFrame(SDL_UserEvent* event) override;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) override;
|
virtual void dropFrame(SDL_UserEvent* event) override;
|
||||||
|
|
||||||
|
virtual IFFmpegRenderer* getRenderer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool
|
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
chooseDecoder(
|
int videoFormat, int width, int height,
|
||||||
StreamingPreferences::VideoDecoderSelection vds,
|
int maxFps, bool enableVsync, bool testOnly);
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
|
||||||
int width, int height,
|
|
||||||
AVCodec*& chosenDecoder,
|
void reset();
|
||||||
const AVCodecHWConfig*& chosenHwConfig,
|
|
||||||
IFFmpegRenderer*& newRenderer);
|
static
|
||||||
|
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||||
|
const enum AVPixelFormat* pixFmts);
|
||||||
|
|
||||||
AVPacket m_Pkt;
|
AVPacket m_Pkt;
|
||||||
AVCodecContext* m_VideoDecoderCtx;
|
AVCodecContext* m_VideoDecoderCtx;
|
||||||
QByteArray m_DecodeBuffer;
|
QByteArray m_DecodeBuffer;
|
||||||
const AVCodecHWConfig* m_HwDecodeCfg;
|
const AVCodecHWConfig* m_HwDecodeCfg;
|
||||||
IFFmpegRenderer* m_Renderer;
|
IFFmpegRenderer* m_Renderer;
|
||||||
|
SDL_atomic_t m_QueuedFrames;
|
||||||
|
int m_ConsecutiveFailedDecodes;
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
|
||||||
|
static const uint8_t k_H264TestFrame[];
|
||||||
|
static const uint8_t k_HEVCTestFrame[];
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include "ffmpeg.h"
|
||||||
|
|
||||||
|
// 720p 60 FPS H.264 with 1 reference frame
|
||||||
|
const uint8_t FFmpegVideoDecoder::k_H264TestFrame[] = {
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20, 0xac, 0x2b, 0x40, 0x28, 0x02, 0xdd, 0x80, 0xb5, 0x06, 0x06, 0x06, 0xa5, 0x00, 0x00, 0x03, 0x03, 0xe8, 0x00, 0x01, 0xd4, 0xc0, 0x8f, 0x4a, 0xa0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0xb0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x65, 0xb8, 0x02, 0x01, 0x67, 0x25, 0x1b, 0xf4, 0xfa, 0x7d, 0x40, 0x1a, 0x78, 0xe5, 0x10, 0x52, 0xc2, 0xee, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc6, 0xef, 0xbb, 0x81, 0x85, 0x2d, 0x47, 0xda, 0xca, 0x4c, 0x00, 0x00, 0x03, 0x00, 0x02, 0x7b, 0xcf, 0x80, 0x00, 0x45, 0x40, 0x01, 0x8d, 0xa6, 0x00, 0x01, 0x64, 0x00, 0x0e, 0x03, 0xc8, 0x00, 0x0e, 0x10, 0x00, 0xbd, 0xc5, 0x00, 0x01, 0x11, 0x00, 0x0e, 0xa3, 0x80, 0x00, 0x38, 0xa0,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x6e, 0x2e, 0x00, 0x83, 0x7f, 0xb4, 0x8e, 0x79, 0xa5, 0xff, 0x84, 0x3f, 0x7f, 0x34, 0x3f, 0x97, 0x1b, 0xaf, 0x31, 0x5f, 0x6e, 0xaa, 0xb6, 0xac, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x78, 0x36, 0x9d, 0xa4, 0x65, 0xf9, 0x1e, 0x5b, 0x3a, 0xb0, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x41, 0x80, 0x00, 0xc5, 0xc8, 0x00, 0x00, 0xfa, 0x00, 0x03, 0x24, 0x00, 0x0e, 0x20, 0x00, 0x3f, 0x80, 0x01, 0x32, 0x00, 0x08, 0x68, 0x00, 0x3e, 0xc0, 0x03, 0x8a,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x37, 0x0b, 0x80, 0x21, 0x7f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xae, 0x1a, 0xbb, 0xae, 0x75, 0x9e, 0x35, 0xae, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x03, 0x64, 0x00, 0x09, 0x98, 0x00, 0x1e, 0xc0, 0x00, 0x64, 0x80, 0x01, 0xc4, 0x00, 0x07, 0xf0, 0x00, 0x42, 0xf0, 0x00, 0x00, 0xe1, 0x98, 0x00, 0x05, 0x4b, 0x28, 0x00, 0x06, 0x04,
|
||||||
|
0x00, 0x00, 0x01, 0x65, 0x00, 0x14, 0xa2, 0xe0, 0x08, 0x5f, 0xe3, 0x85, 0x1c, 0xd9, 0xff, 0xe1, 0x1b, 0x01, 0xfa, 0xc0, 0x3e, 0x11, 0x7e, 0xfe, 0x45, 0x5c, 0x43, 0x69, 0x31, 0x4b, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x02, 0x24, 0xb9, 0x7d, 0xb4, 0x70, 0x4d, 0x15, 0xc5, 0x0a, 0x4e, 0x60, 0x00, 0x00, 0x03, 0x00, 0x00, 0x26, 0xa8, 0xb0, 0x00, 0x13, 0xcd, 0xcc, 0x00, 0x07, 0x48, 0x88, 0x00, 0x06, 0x29, 0x69, 0x00, 0x01, 0x16, 0xac, 0x80, 0x00, 0x9e, 0x4e, 0x80, 0x00, 0x3f, 0x84, 0x20, 0x00, 0x6f, 0x41, 0xa0, 0x00, 0x20, 0x00, 0x02, 0x16, 0xb8, 0x00, 0x08, 0x08
|
||||||
|
};
|
||||||
|
|
||||||
|
// 720p 60 FPS HEVC with 1 reference frame
|
||||||
|
const uint8_t FFmpegVideoDecoder::k_HEVCTestFrame[] = {
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xac, 0x09,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0xa0, 0x02, 0x80, 0x80, 0x2e, 0x1f, 0x13, 0x96, 0xb4, 0xa4, 0x25, 0x92, 0xe3, 0x01, 0x6a, 0x0c, 0x0c, 0x0d, 0x48, 0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00, 0x07, 0x85, 0xf1, 0xa2, 0xd0,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0xf7, 0xc0, 0xcc, 0x90,
|
||||||
|
0x00, 0x00, 0x00, 0x01, 0x2a, 0x01, 0xac, 0x0c, 0xf0, 0xe6, 0xf6, 0x5e, 0xff, 0xd9, 0x97, 0xeb, 0x44, 0x7d, 0x1f, 0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x52, 0x7c, 0x4e, 0x5b, 0xfe, 0xbe, 0xc2, 0x87, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x2b, 0x60,
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#include "sl.h"
|
||||||
|
|
||||||
|
SLVideoDecoder::SLVideoDecoder()
|
||||||
|
: m_VideoContext(nullptr),
|
||||||
|
m_VideoStream(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SLVideoDecoder::~SLVideoDecoder()
|
||||||
|
{
|
||||||
|
if (m_VideoStream != nullptr) {
|
||||||
|
SLVideo_FreeStream(m_VideoStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_VideoContext != nullptr) {
|
||||||
|
SLVideo_FreeContext(m_VideoContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SLVideoDecoder::isHardwareAccelerated()
|
||||||
|
{
|
||||||
|
// SLVideo is always hardware accelerated
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
SLVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
SDL_Window*,
|
||||||
|
int videoFormat, int, int, int frameRate, bool)
|
||||||
|
{
|
||||||
|
// SLVideo only supports hardware decoding
|
||||||
|
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SLVideo only supports H.264
|
||||||
|
if (videoFormat != VIDEO_FORMAT_H264) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_VideoContext = SLVideo_CreateContext();
|
||||||
|
if (!m_VideoContext) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo_CreateContext() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a low latency H.264 stream
|
||||||
|
m_VideoStream = SLVideo_CreateStream(m_VideoContext, k_ESLVideoFormatH264, 1);
|
||||||
|
if (!m_VideoStream) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo_CreateStream() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLVideo_SetStreamTargetFramerate(m_VideoStream, frameRate, 1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
SLVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = SLVideo_BeginFrame(m_VideoStream, du->fullLength);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo_BeginFrame() failed: %d",
|
||||||
|
err);
|
||||||
|
|
||||||
|
// Need an IDR frame to resync
|
||||||
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
PLENTRY entry = du->bufferList;
|
||||||
|
while (entry != nullptr) {
|
||||||
|
err = SLVideo_WriteFrameData(m_VideoStream,
|
||||||
|
entry->data,
|
||||||
|
entry->length);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo_WriteFrameData() failed: %d",
|
||||||
|
err);
|
||||||
|
|
||||||
|
// Need an IDR frame to resync
|
||||||
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = entry->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SLVideo_SubmitFrame(m_VideoStream);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLVideo_SubmitFrame() failed: %d",
|
||||||
|
err);
|
||||||
|
|
||||||
|
// Need an IDR frame to resync
|
||||||
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DR_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "decoder.h"
|
||||||
|
|
||||||
|
#include <SLVideo.h>
|
||||||
|
|
||||||
|
class SLVideoDecoder : public IVideoDecoder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SLVideoDecoder();
|
||||||
|
virtual ~SLVideoDecoder();
|
||||||
|
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
SDL_Window* window,
|
||||||
|
int videoFormat,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
int frameRate,
|
||||||
|
bool enableVsync);
|
||||||
|
virtual bool isHardwareAccelerated();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
|
// Unused since rendering is done directly from the decode thread
|
||||||
|
virtual void renderFrame(SDL_UserEvent*) {}
|
||||||
|
virtual void dropFrame(SDL_UserEvent*) {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CSLVideoContext* m_VideoContext;
|
||||||
|
CSLVideoStream* m_VideoStream;
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define THROW_BAD_ALLOC_IF_NULL(x) \
|
#define THROW_BAD_ALLOC_IF_NULL(x) \
|
||||||
if ((x) == nullptr) throw new std::bad_alloc()
|
if ((x) == nullptr) throw std::bad_alloc()
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
SOURCES = main.cpp
|
||||||
|
LIBS += -lSLVideo
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <SLVideo.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
SLVideo_CreateContext();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Headers
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Resources
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Versions/Current/SDL2
|
||||||