Compare commits

...
74 Commits
Author SHA1 Message Date
Cameron Gutman 9beb51ecac Version 0.1.0 2018-08-05 19:36:30 -07:00
Cameron Gutman 7be167856e Only enable game optimizations when streaming at officially supported resolutions 2018-08-05 18:09:35 -07:00
Cameron Gutman e1d68ef097 Fix Y values wrapping at -32768 2018-08-05 18:01:50 -07:00
Cameron Gutman 5ec256a734 Fix QML warning 2018-08-05 17:09:29 -07:00
Cameron Gutman e4d7d190e2 Suppress logging for applist queries 2018-08-05 17:03:05 -07:00
Cameron Gutman b6763bb9f0 Fix VDPAU rendering at 4K 2018-08-05 16:33:08 -07:00
Cameron Gutman 6d9c0c96c9 Fix streaming with non-aligned resolutions with DXVA2 on my Quadro P400 2018-08-05 15:25:40 -07:00
Cameron Gutman 73dd5630df Fix FPS setting restore logic 2018-08-05 15:09:36 -07:00
Cameron Gutman b76645e26b Fix resolution comparision issue with portrait displays 2018-08-05 15:03:24 -07:00
Cameron Gutman 9cc20c27ab Add support for native resolution streaming 2018-08-05 14:55:26 -07:00
Cameron Gutman 36b42f6e50 Suppress log spam from serverinfo queries 2018-08-05 14:06:42 -07:00
Cameron Gutman 45ebf2ca7d Add log messages for server GPU, bitrate, and slices 2018-08-05 13:32:04 -07:00
Cameron Gutman 2140c94cd1 Fix status icon centering on PC view. Fixes #10 2018-08-05 12:47:08 -07:00
Cameron Gutman 470769e910 Replace running status text with app icon overlay 2018-08-05 12:19:54 -07:00
Cameron Gutman 493165f57f Add a busy indicator while waiting for initial contact with a PC 2018-08-05 12:13:08 -07:00
Cameron Gutman 1dcf55515b Initial replacement of status text with status icons on the PC view 2018-08-05 11:47:14 -07:00
Cameron Gutman b04bc5117d Fix scaling logic in the Linux renderers using the new helper function 2018-08-04 22:31:14 -07:00
Cameron Gutman 5cbb38091b Create new utility function for scaling with aspect ratio and use it for DXVA2 2018-08-04 22:22:15 -07:00
Cameron Gutman b076744f00 Support HEVC software decoding and forcing HEVC without GPU support 2018-08-04 16:45:31 -07:00
Cameron Gutman 723e815546 Add warning for forcing software decoding 2018-08-04 16:15:13 -07:00
Cameron Gutman ca72cbb135 Implement displaying launch warnings. Fixes #12 2018-08-04 16:05:37 -07:00
Cameron Gutman 1e8c2b179a Only fill the screen in windowed mode if the stream is larger than usable display area 2018-08-04 15:34:56 -07:00
Cameron Gutman f2ed3a8502 Make stream window resizable 2018-08-04 15:34:56 -07:00
Cameron Gutman 2bcc15e1d2 Rebuild FFmpeg for Windows to reduce install size 2018-08-04 02:12:30 -07:00
Cameron Gutman 4935baa72a Rebuild FFmpeg libraries for Mac to reduce package size 2018-08-04 01:11:19 -07:00
Cameron Gutman 10cfaa2a5f Fix Linux CI 2018-08-03 18:59:37 -07:00
Cameron Gutman 3117e360f4 Retry surface creation to work around transient VDP_STATUS_RESOURCES errors 2018-08-03 18:26:55 -07:00
Cameron Gutman 48a770edec Enable HEVC support on NVIDIA GPUs with VDPAU 2018-08-03 02:49:43 -07:00
Cameron Gutman fca402138f Add VDPAU renderer 2018-08-03 02:11:44 -07:00
Cameron Gutman 2853e498b0 Minor debugging improvements 2018-08-02 23:24:44 -07:00
Cameron Gutman 509dba2264 Enable ffmpeg logging during codec initialization 2018-08-02 22:48:40 -07:00
Cameron Gutman b8dd131af8 Allow gamepad input when the app doesn't have focus. Fixes #23 2018-08-02 22:36:44 -07:00
Cameron Gutman 0c18bcdd5e Fix DXVA 2 regression on Windows 2018-08-02 22:28:59 -07:00
Cameron Gutman 624578f286 Fix decoder reinitialization after test frame 2018-08-02 21:57:16 -07:00
Cameron Gutman c18e99c9ec Validate that the codec is functional before using it. Fixes streaming failure with VAAPI if hardware lacks codec support 2018-08-02 21:37:46 -07:00
Cameron Gutman 979ba2dab7 Version 0.0.5 2018-08-01 23:00:55 -07:00
Cameron Gutman 56277af5d1 Fix quit error message when another PC started the streaming session 2018-08-01 22:43:20 -07:00
Cameron Gutman 608db6a3e5 Implement support for quitting apps 2018-08-01 22:32:21 -07:00
Cameron Gutman be52272e5c Fix state corruption caused by having 2 separate ComputerModels. Fixes #6 2018-08-01 21:29:03 -07:00
Cameron Gutman 32f305347f Bundle ANGLE libraries into the installer to load on hardware without good OpenGL support 2018-08-01 20:40:05 -07:00
Cameron Gutman 18dd6b09bb Remove 10 ms sleep hidden inside SDL_WaitEvent(). This fixes mouse polling rate being capped at 100 Hz (1 second / 10 ms) and the rendering FPS being capped at 90 FPS. 2018-08-01 18:26:50 -07:00
Cameron Gutman 3ed5f9edf7 Implement C++ functions for quitting apps and querying running apps 2018-07-31 22:21:39 -07:00
Cameron Gutman 017362a5d1 Only allow waking PCs that are not online and paired 2018-07-31 22:10:38 -07:00
Cameron Gutman e250e08242 Fix AMD vendor ID 2018-07-31 18:22:10 -07:00
Cameron Gutman 48d5a7fb51 Add code signing for Windows. Fixes #11 2018-07-31 18:16:39 -07:00
Cameron Gutman 141ee11e2f Avoid HEVC on GPUs using hybrid decoding. Fixes #14 2018-07-31 18:09:09 -07:00
Cameron Gutman 4e82ea298e Update AppStream metadata for 0.0.4a 2018-07-30 23:42:18 -07:00
Cameron Gutman 22be912788 Fix accent prompt appearing when keys are held down on macOS 2018-07-30 22:44:19 -07:00
Cameron Gutman 02242b9b34 Replace Opus submodule with prebuilt libopus 1.2.1 binaries for Windows and Mac 2018-07-30 22:10:08 -07:00
Cameron Gutman e9c1f883da Use full-screen desktop mode for Mac to allow Spaces to work 2018-07-30 00:03:17 -07:00
Cameron Gutman 141f6669e8 Version 0.0.4 2018-07-29 21:44:10 -07:00
Cameron Gutman 6300d242cc Update AppStream metadata 2018-07-29 21:40:34 -07:00
Cameron Gutman 186579266c Fix Online -> Offline computer transition 2018-07-29 19:08:29 -07:00
Cameron Gutman 13490af90c Replace OpenSSL dylibs with static build for macOS 10.10+ 2018-07-29 19:07:42 -07:00
R. Aidan Campbell 23a9c2cd9e context menu opened by default for offline hosts 2018-07-29 16:13:42 -07:00
R. Aidan Campbell 88631e4a63 Merge branch 'master' of github.com:cgutman/moonlight-qt 2018-07-29 16:04:53 -07:00
R. Aidan Campbell 9e744ce169 Now with WoL and PC deletion from QML. Deletion needs bugfix, possibly not committed to storage? 2018-07-29 16:04:45 -07:00
Cameron Gutman 77a68db8c1 Code signing for Mac 2018-07-29 15:35:51 -07:00
Cameron Gutman 27247f253c Add Travis CI badge 2018-07-29 13:50:11 -07:00
Cameron Gutman ada7f92c75 Travis CI for Mac 2018-07-29 13:38:05 -07:00
Cameron Gutman 87db4e88a5 Replace autogenerate app bundle strings with real ones and relocate the icon for Mac 2018-07-29 13:05:18 -07:00
R. Aidan Campbell 277594139b Updated macos .app icon to use proper logo 2018-07-29 10:21:53 -07:00
Cameron Gutman 79d5e35306 Mac deployment
- Use the SDL2 framework wholesale instead of manually extracting dylib and headers
- Run install_name_tool on the remaining dylibs so they use the correct load path
- Add generate-dmg.sh for creating packaged Mac builds
2018-07-29 03:38:59 -07:00
Cameron Gutman cda80b319e Publish artifacts from AppVeyor builds 2018-07-28 23:59:20 -07:00
Cameron Gutman ab4b024df4 Travis CI support 2018-07-28 22:49:36 -07:00
Cameron Gutman 7f9cbac232 Fix more AppVeyor bugs 2018-07-28 19:56:08 -07:00
Cameron Gutman 8470ffcb0d Fix some build script bugs uncovered by AppVeyor 2018-07-28 19:44:09 -07:00
Cameron Gutman 92c897762b Fix AppVeyor link 2018-07-28 19:33:11 -07:00
Cameron Gutman 36014cfcfd Add AppVeyor build status 2018-07-28 19:29:54 -07:00
Cameron Gutman df26f731eb Improve debugging messages 2018-07-28 16:06:26 -07:00
Cameron Gutman e39b5a6a72 Block x86 installs on x64 OSes 2018-07-28 13:59:27 -07:00
Cameron Gutman c7138fe36c Fix Discord link 2018-07-28 03:52:24 -07:00
Cameron Gutman b5deab772d Add Discord link 2018-07-28 03:47:34 -07:00
Cameron Gutman 4bfeccec74 Update README with more information 2018-07-28 03:45:45 -07:00
161 changed files with 8482 additions and 673 deletions
-3
View File
@@ -1,9 +1,6 @@
[submodule "moonlight-common-c/moonlight-common-c"]
path = moonlight-common-c/moonlight-common-c
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"]
path = qmdnsengine/qmdnsengine
url = https://github.com/cgutman/qmdnsengine.git
+46
View File
@@ -0,0 +1,46 @@
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
- libesd0-dev
- libgl-dev
- 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
+17 -1
View File
@@ -1,9 +1,25 @@
# 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).
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).
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?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 on high refresh rate monitors
- Supports streaming at 720p, 1080p, 1440p, or 4K
- 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
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
+9 -5
View File
@@ -5,22 +5,26 @@
<key>CFBundleExecutable</key>
<string>Moonlight</string>
<key>CFBundleGetInfoString</key>
<string>Created by Qt/QMake</string>
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
<key>CFBundleIconFile</key>
<string>macos</string>
<string>moonlight</string>
<key>CFBundleIdentifier</key>
<string>com.yourcompany.moonlight-qt</string>
<string>com.moonlight-stream.Moonlight</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.10</string>
<key>NOTE</key>
<string>This file was generated by Qt/QMake.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleDisplayName</key>
<string>Moonlight</string>
</dict>
</plist>
+28 -20
View File
@@ -8,6 +8,9 @@ unix:!macx {
TARGET = Moonlight
}
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
@@ -34,13 +37,13 @@ win32 {
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include
LIBS += -L$$PWD/../libs/mac/lib
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
}
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2
PKGCONFIG += openssl sdl2 opus
LIBS += -ldl
packagesExist(libavcodec) {
@@ -50,14 +53,18 @@ unix:!macx {
packagesExist(libva) {
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
}
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg
}
macx {
LIBS += -lssl -lcrypto -lSDL2 -lavcodec.58 -lavutil.56
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
CONFIG += ffmpeg
}
@@ -74,7 +81,8 @@ SOURCES += \
streaming/session.cpp \
streaming/audio.cpp \
gui/computermodel.cpp \
gui/appmodel.cpp
gui/appmodel.cpp \
streaming/streamutils.cpp
HEADERS += \
utils.h \
@@ -88,7 +96,8 @@ HEADERS += \
streaming/session.hpp \
gui/computermodel.h \
gui/appmodel.h \
streaming/video/decoder.h
streaming/video/decoder.h \
streaming/streamutils.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -109,6 +118,13 @@ libva {
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
}
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)
@@ -147,13 +163,6 @@ else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
INCLUDEPATH += $$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
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
@@ -182,16 +191,15 @@ unix:!macx: {
INSTALLS += target desktop icons appdata
}
win32 {
RC_ICONS = moonlight.ico
}
macx {
QMAKE_INFO_PLIST = $$PWD/Info.plist
APP_QML_FILES.files = res/macos.icns
APP_QML_FILES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_QML_FILES
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
}
VERSION = 0.0.3
VERSION = 0.1.0
+19
View File
@@ -51,6 +51,8 @@ NvComputer::NvComputer(QSettings& settings)
this->appVersion = nullptr;
this->maxLumaPixelsHEVC = 0;
this->serverCodecModeSupport = 0;
this->pendingQuit = false;
this->gpuModel = nullptr;
}
void
@@ -133,8 +135,10 @@ NvComputer::NvComputer(QString address, QString serverInfo)
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = address;
this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false;
}
bool NvComputer::wake()
@@ -264,6 +268,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
return changed;
@@ -446,6 +451,15 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
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()
{
QWriteLocker lock(&m_Lock);
@@ -497,6 +511,11 @@ ComputerManager::handleComputerStateChanged(NvComputer* computer)
{
emit computerStateChanged(computer);
if (computer->pendingQuit && computer->currentGameId == 0) {
computer->pendingQuit = false;
emit quitAppCompleted(QVariant());
}
// Save updated hosts to QSettings
saveHosts();
}
+58 -11
View File
@@ -16,10 +16,14 @@
class NvComputer
{
friend class PcMonitorThread;
friend class ComputerManager;
friend class PendingQuitTask;
private:
void sortAppList();
bool pendingQuit;
public:
explicit NvComputer(QString address, QString serverInfo);
@@ -61,6 +65,7 @@ public:
QVector<NvDisplayMode> displayModes;
int maxLumaPixelsHEVC;
int serverCodecModeSupport;
QString gpuModel;
// Persisted traits
QString localAddress;
@@ -148,31 +153,26 @@ private:
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) {
bool stateChanged = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING; i++) {
bool online = false;
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
}
if (tryPollComputer(address, stateChanged)) {
online = true;
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
// Note: we don't need to acquire the read lock here,
// because we're on the writing thread.
if (m_Computer->state != NvComputer::CS_ONLINE) {
if (m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
m_Computer->state = NvComputer::CS_OFFLINE;
stateChanged = true;
}
// Grab the applist if it's empty or it's been long enough that we need to refresh
@@ -259,6 +259,8 @@ public:
void pairHost(NvComputer* computer, QString pin);
void quitRunningApp(NvComputer* computer);
QVector<NvComputer*> getComputers();
// computer is deleted inside this call
@@ -271,6 +273,8 @@ signals:
void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error);
private slots:
void handleComputerStateChanged(NvComputer* computer);
@@ -338,6 +342,49 @@ private:
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
{
Q_OBJECT
+17 -6
View File
@@ -67,6 +67,7 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
true,
true);
// Throws if the request failed
verifyResponseStatus(serverInfo);
@@ -79,6 +80,7 @@ NvHTTP::getServerInfo()
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
true,
true);
verifyResponseStatus(serverInfo);
}
@@ -225,6 +227,7 @@ NvHTTP::getAppList()
QString appxml = openConnectionToString(m_BaseUrlHttps,
"applist",
nullptr,
true,
true);
verifyResponseStatus(appxml);
@@ -338,9 +341,10 @@ QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout)
bool enableTimeout,
bool suppressLogging)
{
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
QString ret;
QTextStream stream(reply);
@@ -355,7 +359,8 @@ QNetworkReply*
NvHTTP::openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout)
bool enableTimeout,
bool suppressLogging)
{
// Build a URL for the request
QUrl url(baseUrl);
@@ -381,13 +386,17 @@ NvHTTP::openConnection(QUrl baseUrl,
{
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
}
qInfo() << "Executing request:" << url.toString();
if (!suppressLogging) {
qInfo() << "Executing request:" << url.toString();
}
loop.exec(QEventLoop::ExcludeUserInputEvents);
// Abort the request if it timed out
if (!reply->isFinished())
{
qWarning() << "Aborting timed out request for" << url.toString();
if (!suppressLogging) {
qWarning() << "Aborting timed out request for" << url.toString();
}
reply->abort();
}
@@ -398,7 +407,9 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
qWarning() << command << " request failed with error " << reply->error();
if (!suppressLogging) {
qWarning() << command << " request failed with error " << reply->error();
}
GfeHttpResponseException exception(reply->error(), reply->errorString());
delete reply;
throw exception;
+4 -2
View File
@@ -107,7 +107,8 @@ public:
openConnectionToString(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout);
bool enableTimeout,
bool suppressLogging = false);
static
QVector<int>
@@ -143,7 +144,8 @@ private:
openConnection(QUrl baseUrl,
QString command,
QString arguments,
bool enableTimeout);
bool enableTimeout,
bool suppressLogging = false);
QString m_Address;
QNetworkAccessManager m_Nam;
@@ -1,20 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>com.moonlight_stream.Moonlight.desktop</id>
<?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 or other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
<p>GameStream utilizes the NVENC hardware encoder present on NVIDIA GPUs for low-latency streaming at up 4K 60 FPS. Moonlight can utilize VAAPI hardware acceleration for high-performance H.264 and HEVC video decoding with low CPU usage.</p>
<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 on high refresh rate monitors</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.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>
@@ -29,33 +84,5 @@
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.1">
<content_attribute id="violence-cartoon">none</content_attribute>
<content_attribute id="violence-fantasy">none</content_attribute>
<content_attribute id="violence-realistic">none</content_attribute>
<content_attribute id="violence-bloodshed">none</content_attribute>
<content_attribute id="violence-sexual">none</content_attribute>
<content_attribute id="violence-desecration">none</content_attribute>
<content_attribute id="violence-slavery">none</content_attribute>
<content_attribute id="violence-worship">none</content_attribute>
<content_attribute id="drugs-alcohol">none</content_attribute>
<content_attribute id="drugs-narcotics">none</content_attribute>
<content_attribute id="drugs-tobacco">none</content_attribute>
<content_attribute id="sex-nudity">none</content_attribute>
<content_attribute id="sex-themes">none</content_attribute>
<content_attribute id="sex-homosexuality">none</content_attribute>
<content_attribute id="sex-prostitution">none</content_attribute>
<content_attribute id="sex-adultery">none</content_attribute>
<content_attribute id="sex-appearance">none</content_attribute>
<content_attribute id="language-profanity">none</content_attribute>
<content_attribute id="language-humor">none</content_attribute>
<content_attribute id="language-discrimination">none</content_attribute>
<content_attribute id="social-chat">none</content_attribute>
<content_attribute id="social-info">moderate</content_attribute>
<content_attribute id="social-audio">none</content_attribute>
<content_attribute id="social-location">none</content_attribute>
<content_attribute id="social-contacts">none</content_attribute>
<content_attribute id="money-purchasing">none</content_attribute>
<content_attribute id="money-gambling">none</content_attribute>
</content_rating>
<content_rating type="oars-1.0" />
</component>
+88 -13
View File
@@ -1,4 +1,5 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.3
import QtQuick.Controls 2.2
import AppModel 1.0
@@ -50,12 +51,6 @@ GridView {
delegate: Item {
width: 200; height: 300;
Component.onCompleted: {
if (model.running) {
appNameText.text = "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + appNameText.text
}
}
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
@@ -66,11 +61,21 @@ 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 {
id: appNameText
text: model.name
color: "white"
width: parent.width
height: 125
anchors.top: appIcon.bottom
@@ -80,16 +85,86 @@ GridView {
elide: Text.ElideRight
}
function launchOrResumeSelectedApp()
{
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView)
segue.appName = model.name
segue.session = appModel.createSessionForApp(index)
stackView.push(segue)
}
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : "";
property bool segueToStream : false
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 segue = component.createObject(stackView)
segue.appName = appName
if (segueToStream) {
// Store the session and app name if we're going to stream after
// successfully quitting the old app.
segue.nextAppName = model.name
segue.nextSession = appModel.createSessionForApp(index)
}
else {
segue.nextAppName = null
segue.nextSession = null
}
stackView.push(segue)
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
// TODO: Check if a different game is running
if (mouse.button === Qt.LeftButton) {
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
else {
// 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
}
}
}
+90 -59
View File
@@ -81,14 +81,14 @@ GridView {
return model
}
model: createModel()
model: computerModel
delegate: Item {
width: 300; height: 300;
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter;
anchors.horizontalCenter: parent.horizontalCenter
source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
@@ -98,6 +98,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 {
id: pcNameText
text: model.name
@@ -110,76 +134,70 @@ GridView {
wrapMode: Text.Wrap
}
Text {
function getStatusText(model)
{
if (model.online) {
var text = "<font color=\"green\">Online</font>"
text += "<font color=\"white\"> - </font>"
if (model.paired) {
text += "<font color=\"skyblue\">Paired</font>"
}
else if (model.busy) {
text += "<font color=\"red\">Busy</font>"
}
else {
text += "<font color=\"red\">Not Paired</font>"
}
return text
}
else {
return "<font color=\"red\">Offline</font>";
Menu {
id: pcContextMenu
MenuItem {
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
height: visible ? implicitHeight : 0
}
MenuItem {
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
}
}
id: pcPairedText
text: getStatusText(model)
visible: !model.addPc
width: parent.width
anchors.top: pcNameText.bottom
font.pointSize: 24
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton;
onClicked: {
if (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)
appView.computerIndex = index
appView.objectName = model.name
stackView.push(appView)
if(mouse.button === Qt.LeftButton) {
if (model.addPc) {
addPcDialog.open()
}
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 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 {
// 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()
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 if(!model.online) {
pcContextMenu.open()
}
}
else {
// TODO: Wake on LAN and delete PC options
else { // right click
if(!model.addPc) { // but only for actual PCs, not the add-pc option
pcContextMenu.open()
}
}
}
}
@@ -218,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 {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
+86
View File
@@ -0,0 +1,86 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.3
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)
segue.appName = nextAppName
segue.session = nextSession
stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
// We must be polling or we won't get the quitAppCompleted() callback
ComputerManager.startPolling()
}
else {
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
// Stop polling when this view is not on top
ComputerManager.stopPollingAsync()
}
}
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
}
}
+45 -8
View File
@@ -43,7 +43,7 @@ ScrollView {
Label {
width: parent.width
id: resFPSdesc
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors")
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
font.pointSize: 9
wrapMode: Text.Wrap
color: "white"
@@ -55,6 +55,41 @@ ScrollView {
ComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: {
// Add native resolutions for all attached displays
for (var displayIndex = 0;; displayIndex++) {
var screenRect = prefs.getDisplayResolution(displayIndex)
if (screenRect.width === 0) {
// Exceeded max count of displays
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
@@ -63,17 +98,21 @@ ScrollView {
for (var i = 0; i < resolutionComboBox.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width);
var el_height = parseInt(resolutionListModel.get(i).video_height);
if (saved_width === el_width && saved_height === el_height) {
// Pick the highest value lesser or equal to the saved resolution
if (saved_width * saved_height >= el_width * el_height) {
currentIndex = i
}
}
}
id: resolutionComboBox
font.pointSize: 9
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"
@@ -121,14 +160,15 @@ ScrollView {
currentIndex = 0
for (var i = 0; i < fpsComboBox.count; i++) {
var el_fps = parseInt(fpsListModel.get(i).video_fps);
if (el_fps === saved_fps) {
// Pick the highest value lesser or equal to the saved FPS
if (saved_fps >= el_fps) {
currentIndex = i
}
}
}
id: fpsComboBox
font.pointSize: 9
textRole: "text"
model: ListModel {
id: fpsListModel
@@ -238,7 +278,6 @@ ScrollView {
id: audioComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: audioListModel
@@ -362,7 +401,6 @@ ScrollView {
id: decoderComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: decoderListModel
@@ -409,7 +447,6 @@ ScrollView {
id: codecComboBox
width: Math.min(bitrateDesc.implicitWidth, parent.width)
font.pointSize: 9
textRole: "text"
model: ListModel {
id: codecListModel
+9 -3
View File
@@ -6,8 +6,8 @@ import Session 1.0
Item {
property Session session
property string appname
property string stageText : "Starting " + appname + "..."
property string appName
property string stageText : "Starting " + appName + "..."
anchors.fill: parent
@@ -47,7 +47,13 @@ Item {
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: {
+31
View File
@@ -19,6 +19,32 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
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)
{
Q_ASSERT(appIndex < m_Apps.count());
@@ -70,6 +96,11 @@ QHash<int, QByteArray> AppModel::roleNames() const
return names;
}
void AppModel::quitRunningApp()
{
m_ComputerManager->quitRunningApp(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer)
{
// Ignore updates for computers that aren't ours
+6
View File
@@ -25,6 +25,12 @@ public:
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;
int rowCount(const QModelIndex &parent) const override;
+6
View File
@@ -46,8 +46,12 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->pairState == NvComputer::PS_PAIRED;
case BusyRole:
return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default:
return QVariant();
}
@@ -74,6 +78,8 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[PairedRole] = "paired";
names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
return names;
}
+2
View File
@@ -13,6 +13,8 @@ class ComputerModel : public QAbstractListModel
OnlineRole,
PairedRole,
BusyRole,
WakeableRole,
StatusUnknownRole,
AddPcRole
};
Binary file not shown.
+1
View File
@@ -6,5 +6,6 @@
<file>gui/SettingsView.qml</file>
<file>gui/StreamSegue.qml</file>
<file>gui/GamepadMapper.qml</file>
<file>gui/QuitSegue.qml</file>
</qresource>
</RCC>
+4
View File
@@ -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

+4
View File
@@ -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

Binary file not shown.
+3
View File
@@ -5,6 +5,9 @@
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/ic_tv_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/settings.svg</file>
<file>res/arrow_left.svg</file>
+41 -6
View File
@@ -82,9 +82,9 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
// Cap the frame rate at 90 FPS, since I can't seem to get
// much more out of GFE even with the game rendering at > 300 FPS.
maxFrameRate = qMax(maxFrameRate, qMin(90, mode.refresh_rate));
// Cap the frame rate at 120 FPS. Past this, the encoders start
// to max out and drop frames.
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
}
}
@@ -93,15 +93,50 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
return maxFrameRate;
}
QRect StreamingPreferences::getDisplayResolution(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_GetCurrentDisplayMode(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_GetCurrentDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
{
if (width * height <= 1280 * 720) {
// This table prefers 16:10 resolutions because they are
// 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 <= 1920 * 1080) {
else if (width * height <= 1920 * 1200) {
return static_cast<int>(10000 * (fps / 30.0));
}
else if (width * height <= 2560 * 1440) {
else if (width * height <= 2560 * 1600) {
return static_cast<int>(20000 * (fps / 30.0));
}
else /* if (width * height <= 3840 * 2160) */ {
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include <QObject>
#include <QRect>
class StreamingPreferences : public QObject
{
@@ -18,6 +19,8 @@ public:
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
void reload();
enum AudioConfig
+4
View File
@@ -142,6 +142,10 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
s_SampleIndex = 0;
s_PendingDrops = s_PendingHardDrops = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
opusConfig->channelCount);
return 0;
}
+12 -2
View File
@@ -2,6 +2,8 @@
#include <SDL.h>
#include "streaming/session.hpp"
#include <QtGlobal>
#define VK_0 0x30
#define VK_A 0x41
@@ -23,6 +25,9 @@ const int SdlInputHandler::k_ButtonMap[] = {
SdlInputHandler::SdlInputHandler(bool multiController)
: m_MultiController(multiController)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// 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));
@@ -438,13 +443,18 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
state->lsX = event->value;
break;
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;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -event->value;
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
+122 -23
View File
@@ -13,10 +13,10 @@
#include "video/sl.h"
#endif
#ifdef Q_OS_WIN32
// Using full-screen desktop allows us to avoid needing to enable V-sync
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
// Using full-screen desktop allows us to avoid needing to enable V-sync on Windows
// and it also avoids some strange flickering issues on my Win7 test machine
// with Intel HD 5500 graphics.
// with Intel HD 5500 graphics. It also allows Spaces to work on macOS.
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
#else
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
@@ -138,6 +138,10 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
}
#endif
#if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
#error No video decoding libraries available!
#endif
// If we reach this, we didn't initialize any decoders successfully
return false;
}
@@ -153,6 +157,9 @@ int Session::drSetup(int videoFormat, int width, int height, int frameRate, void
// don't have to hide and show the SDL window (which seems to
// cause pointer hiding to break on Windows).
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video stream is %dx%dx%d (format 0x%x)",
width, height, frameRate, videoFormat);
return 0;
}
@@ -229,6 +236,8 @@ Session::Session(NvComputer* computer, NvApp& app)
m_DecoderLock(0),
m_NeedsIdr(false)
{
qDebug() << "Server GPU:" << m_Computer->gpuModel;
LiInitializeVideoCallbacks(&m_VideoCallbacks);
m_VideoCallbacks.setup = drSetup;
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
@@ -237,7 +246,11 @@ Session::Session(NvComputer* computer, NvApp& app)
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
// Slice up to 4 times for parallel decode, once slice per core
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(qMin(MAX_SLICES, SDL_GetCPUCount()));
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences.width;
@@ -245,6 +258,11 @@ Session::Session(NvComputer* computer, NvApp& app)
m_StreamConfig.fps = m_Preferences.fps;
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
m_StreamConfig.bitrate);
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesKey),
sizeof(m_StreamConfig.remoteInputAesKey));
@@ -254,6 +272,7 @@ Session::Session(NvComputer* computer, NvApp& app)
switch (m_Preferences.audioConfig)
{
case StreamingPreferences::AC_AUTO:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
break;
case StreamingPreferences::AC_FORCE_STEREO:
@@ -264,6 +283,10 @@ Session::Session(NvComputer* computer, NvApp& app)
break;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
switch (m_Preferences.videoCodecConfig)
{
case StreamingPreferences::VCC_AUTO:
@@ -305,10 +328,30 @@ Session::Session(NvComputer* computer, NvApp& app)
}
}
void Session::emitLaunchWarning(QString text)
{
// Emit the warning to the UI
emit displayLaunchWarning(text);
// Wait a little bit so the user can actually read what we just said.
// This wait is a little longer than the actual toast timeout (3 seconds)
// to allow it to transition off the screen before continuing.
uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
bool Session::validateLaunch()
{
QStringList warningList;
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
if (m_StreamConfig.supportsHevc) {
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
@@ -317,22 +360,21 @@ bool Session::validateLaunch()
VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
// NOTE: HEVC currently uses only 1 slice regardless of what
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
// never use it for software decoding (unless common-c starts
// respecting it for HEVC).
m_StreamConfig.supportsHevc = false;
m_StreamConfig.fps) &&
m_Preferences.videoDecoderSelection != StreamingPreferences::VDS_FORCE_SOFTWARE) {
if (hevcForced) {
emit displayLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
emitLaunchWarning("Using software decoding due to your selection to force HEVC. This may cause poor streaming performance.");
}
else {
emitLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
m_StreamConfig.supportsHevc = false;
}
}
if (hevcForced) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
emitLaunchWarning("Your host PC GPU doesn't support HEVC. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
}
}
}
@@ -343,19 +385,19 @@ bool Session::validateLaunch()
// Check that the app supports HDR
if (!m_App.hdrSupported) {
emit displayLaunchWarning(m_App.name + " doesn't support HDR10.");
emitLaunchWarning(m_App.name + " doesn't support HDR10.");
}
// Check that the server GPU supports HDR
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
emit displayLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
}
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
VIDEO_FORMAT_H265_MAIN10,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps)) {
emit displayLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
emitLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
}
else {
// TODO: Also validate display capabilites
@@ -368,7 +410,7 @@ bool Session::validateLaunch()
if (m_StreamConfig.width >= 3840) {
// Only allow 4K on GFE 3.x+
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
emit displayLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
@@ -376,8 +418,8 @@ bool Session::validateLaunch()
// This list is sorted from least to greatest
else if (m_Computer->displayModes.last().width < 3840 ||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
emit displayLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
m_StreamConfig.width = 1920;
m_StreamConfig.height = 1080;
@@ -408,6 +450,24 @@ void Session::getWindowDimensions(bool fullScreen,
{
int displayIndex = 0;
// If there's a display matching this exact resolution, pick that
// one (for native full-screen streaming). Otherwise, assume
// display 0 for now. TODO: Default to the screen that the Qt window is on
if (fullScreen) {
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
SDL_DisplayMode mode;
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
m_ActiveVideoWidth == mode.w &&
m_ActiveVideoHeight == mode.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found exact resolution match on display: %d",
i);
displayIndex = i;
break;
}
}
}
if (m_Window != nullptr) {
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
if (displayIndex < 0) {
@@ -463,6 +523,13 @@ void Session::getWindowDimensions(bool fullScreen,
width -= left + right;
height -= top + bottom;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
width = m_StreamConfig.width;
height = m_StreamConfig.height;
}
}
}
else {
@@ -487,6 +554,7 @@ void Session::toggleFullscreen()
// that way we have accurate display size metrics (not the size our stream changed it to).
if (!fullScreen) {
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
getWindowDimensions(fullScreen, x, y, width, height);
@@ -495,6 +563,7 @@ void Session::toggleFullscreen()
SDL_SetWindowSize(m_Window, width, height);
if (fullScreen) {
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
}
}
@@ -525,6 +594,18 @@ void Session::exec()
Q_ASSERT(m_Computer->currentGameId == 0 ||
m_Computer->currentGameId == m_App.id);
bool enableGameOptimizations = false;
for (const NvDisplayMode &mode : m_Computer->displayModes) {
if (mode.width == m_StreamConfig.width &&
mode.height == m_StreamConfig.height) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found host supported resolution: %dx%d",
mode.width, mode.height);
enableGameOptimizations = prefs.gameOptimizations;
break;
}
}
try {
NvHTTP http(m_Computer->activeAddress);
if (m_Computer->currentGameId != 0) {
@@ -532,7 +613,7 @@ void Session::exec()
}
else {
http.launchApp(m_App.id, &m_StreamConfig,
prefs.gameOptimizations,
enableGameOptimizations,
prefs.playAudioOnHost,
inputHandler.getAttachedGamepadMask());
}
@@ -611,6 +692,10 @@ void Session::exec()
SDL_SetWindowPosition(m_Window, x, y);
SDL_SetWindowSize(m_Window, width, height);
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
SDL_SetWindowResizable(m_Window, SDL_TRUE);
}
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
@@ -639,6 +724,12 @@ void Session::exec()
SDL_SetRelativeMouseMode(SDL_TRUE);
#endif
// Stop text input. SDL enables it by default
// when we initialize the video subsystem, but this
// causes an IME popup when certain keys are held down
// on macOS.
SDL_StopTextInput();
// Disable the screen saver
SDL_DisableScreenSaver();
@@ -653,7 +744,15 @@ void Session::exec()
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event;
while (SDL_WaitEvent(&event)) {
for (;;) {
// We explicitly use SDL_PollEvent() and SDL_Delay() because
// SDL_WaitEvent() has an internal SDL_Delay(10) inside which
// blocks this thread too long for high polling rate mice and high
// refresh rate displays.
if (!SDL_PollEvent(&event)) {
SDL_Delay(1);
continue;
}
switch (event.type) {
case SDL_QUIT:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
+2
View File
@@ -39,6 +39,8 @@ signals:
private:
bool validateLaunch();
void emitLaunchWarning(QString text);
int getDecoderCapabilities();
int sdlDetermineAudioConfiguration();
+20
View File
@@ -0,0 +1,20 @@
#include "streamutils.h"
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);
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <SDL.h>
class StreamUtils
{
public:
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
};
+141 -51
View File
@@ -1,5 +1,7 @@
#include <initguid.h>
#include "dxva2.h"
#include "../ffmpeg.h"
#include <streaming/streamutils.h>
#include <Limelight.h>
@@ -75,11 +77,8 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
context->hwaccel_context = &m_DXVAContext;
context->get_format = ffGetFormat;
context->get_buffer2 = ffGetBuffer2;
context->opaque = this;
m_Pool = av_buffer_pool_init2(ARRAYSIZE(m_DecSurfaces), this, ffPoolAlloc, nullptr);
if (!m_Pool) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -87,12 +86,15 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DXVA2 accelerated renderer");
return true;
}
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);
if (!frame->buf[0]) {
@@ -107,25 +109,14 @@ int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
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()
{
HRESULT hr;
if (isDecoderBlacklisted()) {
return false;
}
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoDecoderService,
reinterpret_cast<void**>(&m_DecService));
if (FAILED(hr)) {
@@ -208,18 +199,11 @@ bool DXVA2Renderer::initializeDecoder()
return false;
}
int alignment;
// HEVC using DXVA requires 128B alignment
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128;
}
else {
alignment = 16;
}
hr = m_DecService->CreateSurface(FFALIGN(m_VideoWidth, alignment),
FFALIGN(m_VideoHeight, alignment),
// Alignment was already taken care of
SDL_assert(m_Desc.SampleWidth % 16 == 0);
SDL_assert(m_Desc.SampleHeight % 16 == 0);
hr = m_DecService->CreateSurface(m_Desc.SampleWidth,
m_Desc.SampleHeight,
ARRAYSIZE(m_DecSurfaces) - 1,
m_Desc.Format,
D3DPOOL_DEFAULT,
@@ -349,6 +333,103 @@ bool DXVA2Renderer::initializeRenderer()
return true;
}
bool DXVA2Renderer::isDecoderBlacklisted()
{
IDirect3D9* d3d9;
HRESULT hr;
bool result = false;
// TODO: Update for HEVC Main10
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) {
D3DCAPS9 caps;
hr = m_Device->GetDeviceCaps(&caps);
if (SUCCEEDED(hr)) {
D3DADAPTER_IDENTIFIER9 id;
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);
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
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break;
default:
// 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.
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
if ((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
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,
"GetDirect3D() failed: %x", hr);
}
if (result) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for format %x",
m_VideoFormat);
}
return result;
}
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
{
m_VideoFormat = videoFormat;
@@ -378,8 +459,19 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
SDL_RenderPresent(m_SdlRenderer);
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
m_Desc.SampleWidth = m_VideoWidth;
m_Desc.SampleHeight = m_VideoHeight;
int alignment;
// HEVC using DXVA requires 128B alignment
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 128;
}
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;
@@ -500,28 +592,26 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
sample.Start = m_FrameIndex;
sample.End = m_FrameIndex + 1;
sample.SrcSurface = surface;
sample.SrcRect.right = m_Desc.SampleWidth;
sample.SrcRect.bottom = m_Desc.SampleHeight;
sample.SrcRect.right = m_VideoWidth;
sample.SrcRect.bottom = m_VideoHeight;
sample.SampleFormat = m_Desc.SampleFormat;
sample.PlanarAlpha = DXVA2_Fixed32OpaqueAlpha();
// Center in frame and preserve aspect ratio
double srcAspectRatio = (double)m_Desc.SampleWidth / (double)m_Desc.SampleHeight;
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
if (dstAspectRatio < srcAspectRatio) {
// Greater height per width
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
sample.DstRect.top = (m_DisplayHeight - drawHeight) / 2;
sample.DstRect.bottom = sample.DstRect.bottom + drawHeight;
sample.DstRect.right = m_DisplayWidth;
}
else {
// Greater width per height
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
sample.DstRect.bottom = m_DisplayHeight;
sample.DstRect.left = (m_DisplayWidth - drawWidth) / 2;
sample.DstRect.right = sample.DstRect.left + drawWidth;
}
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 = {};
+1 -4
View File
@@ -24,6 +24,7 @@ public:
private:
bool initializeDecoder();
bool initializeRenderer();
bool isDecoderBlacklisted();
static
AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -34,10 +35,6 @@ private:
static
int ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int flags);
static
enum AVPixelFormat ffGetFormat(AVCodecContext*,
const enum AVPixelFormat* pixFmts);
int m_VideoFormat;
int m_VideoWidth;
int m_VideoHeight;
+5 -9
View File
@@ -23,22 +23,18 @@ SdlRenderer::~SdlRenderer()
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SDL software renderer");
return true;
}
bool SdlRenderer::initialize(SDL_Window* window,
int videoFormat,
int,
int width,
int height)
{
// NOTE: HEVC currently uses only 1 slice regardless of what
// we provide in CAPABILITY_SLICES_PER_FRAME(), so we should
// never use it for software decoding (unless common-c starts
// respecting it for HEVC).
if (videoFormat != VIDEO_FORMAT_H264) {
return false;
}
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!m_Renderer) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
+24 -23
View File
@@ -1,4 +1,5 @@
#include "vaapi.h"
#include <streaming/streamutils.h>
#include <dlfcn.h>
@@ -70,7 +71,6 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
return false;
}
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VAAPI,
nullptr, nullptr, 0);
@@ -81,6 +81,14 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
return false;
}
// This quirk is set for the VDPAU wrapper which doesn't work with our VAAPI renderer
if (((AVVAAPIDeviceContext*)((AVHWDeviceContext*)(m_HwContext->data))->hwctx)->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;
}
@@ -88,6 +96,10 @@ bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VAAPI accelerated renderer");
return true;
}
@@ -98,34 +110,23 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
// Center in frame and preserve aspect ratio
int x, y, width, height;
double srcAspectRatio = (double)m_VideoWidth / (double)m_VideoHeight;
double dstAspectRatio = (double)m_DisplayWidth / (double)m_DisplayHeight;
if (dstAspectRatio < srcAspectRatio) {
// Greater height per width
int drawHeight = (int)(m_DisplayWidth / srcAspectRatio);
y = (m_DisplayHeight - drawHeight) / 2;
height = drawHeight;
x = 0;
width = m_DisplayWidth;
}
else {
// Greater width per height
int drawWidth = (int)(m_DisplayHeight * srcAspectRatio);
y = 0;
height = m_DisplayHeight;
x = (m_DisplayWidth - drawWidth) / 2;
width = drawWidth;
}
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);
m_vaPutSurface(vaDeviceContext->display,
surface,
m_XWindow,
0, 0,
m_VideoWidth, m_VideoHeight,
x, y,
width, height,
dst.x, dst.y,
dst.w, dst.h,
NULL, 0, 0);
av_frame_free(&frame);
@@ -47,4 +47,3 @@ private:
int m_DisplayWidth;
int m_DisplayHeight;
};
@@ -0,0 +1,332 @@
#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 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_LogWarn(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;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VDPAU accelerated renderer");
return true;
}
void VDPAURenderer::renderFrame(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 chroma;
uint32_t videoSurfaceWidth, videoSurfaceHeight;
status = m_VdpVideoSurfaceGetParameters(videoSurface, &chroma, &videoSurfaceWidth, &videoSurfaceHeight);
if (status != VDP_STATUS_OK) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VdpVideoSurfaceGetParameters() failed: %s",
m_VdpGetErrorString(status));
av_frame_free(&frame);
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"VDPAU surface size: %dx%d",
videoSurfaceWidth, videoSurfaceHeight);
#define PARAM_COUNT 2
const VdpVideoMixerParameter params[PARAM_COUNT] = {
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
};
const void* const paramValues[PARAM_COUNT] = {
&videoSurfaceWidth,
&videoSurfaceHeight,
};
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));
av_frame_free(&frame);
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);
// The decoder can have this surface back now
av_frame_free(&frame);
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,59 @@
#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);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrame(AVFrame* frame);
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;
};
@@ -253,6 +253,10 @@ public:
virtual bool prepareDecoderContext(AVCodecContext* context) override
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using VideoToolbox accelerated renderer");
return true;
}
+184 -96
View File
@@ -13,93 +13,39 @@
#include "ffmpeg-renderers/vaapi.h"
#endif
bool FFmpegVideoDecoder::chooseDecoder(
StreamingPreferences::VideoDecoderSelection vds,
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 Q_OS_WIN32
case AV_HWDEVICE_TYPE_DXVA2:
newRenderer = new DXVA2Renderer();
break;
#ifdef HAVE_LIBVDPAU
#include "ffmpeg-renderers/vdpau.h"
#endif
#ifdef Q_OS_DARWIN
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
newRenderer = VTRendererFactory::createRenderer();
break;
#endif
#ifdef HAVE_LIBVA
case AV_HWDEVICE_TYPE_VAAPI:
newRenderer = new VAAPIRenderer();
break;
#endif
default:
continue;
}
if (newRenderer->initialize(window, videoFormat, width, height)) {
chosenHwConfig = config;
return true;
}
else {
// Failed to initialize
delete newRenderer;
newRenderer = nullptr;
}
}
}
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
bool FFmpegVideoDecoder::isHardwareAccelerated()
{
return m_HwDecodeCfg != nullptr;
}
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()
: m_VideoDecoderCtx(nullptr),
m_DecodeBuffer(1024 * 1024, 0),
@@ -114,6 +60,16 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
}
FFmpegVideoDecoder::~FFmpegVideoDecoder()
{
reset();
}
IFFmpegRenderer* FFmpegVideoDecoder::getRenderer()
{
return m_Renderer;
}
void FFmpegVideoDecoder::reset()
{
// Drop any frames still queued to ensure
// they are properly freed.
@@ -140,24 +96,11 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
avcodec_free_context(&m_VideoDecoderCtx);
delete m_Renderer;
m_Renderer = nullptr;
}
bool FFmpegVideoDecoder::initialize(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int)
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoFormat, int width, int height, bool testOnly)
{
AVCodec* decoder;
if (!chooseDecoder(vds, window, videoFormat, width, height,
decoder, m_HwDecodeCfg, m_Renderer)) {
// Error logged in chooseDecoder()
return false;
}
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
if (!m_VideoDecoderCtx) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -182,12 +125,20 @@ bool FFmpegVideoDecoder::initialize(
m_VideoDecoderCtx->width = width;
m_VideoDecoderCtx->height = height;
m_VideoDecoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // FIXME: HDR
m_VideoDecoderCtx->get_format = ffGetFormat;
// Allow the renderer to attach data to this decoder
if (!m_Renderer->prepareDecoderContext(m_VideoDecoderCtx)) {
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);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -196,9 +147,148 @@ bool FFmpegVideoDecoder::initialize(
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;
}
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)
{
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) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
return true;
}
else {
reset();
return false;
}
}
m_Renderer = createAcceleratedRenderer(config);
if (!m_Renderer) {
continue;
}
m_HwDecodeCfg = config;
// Submit test frame to ensure this codec really works
if (m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, true)) {
// OK, it worked, so now let's initialize it for real
reset();
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
m_Renderer->initialize(window, videoFormat, width, height) &&
completeInitialization(decoder, videoFormat, width, height, false)) {
return true;
}
else {
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
"Decoder failed to initialize after successful test");
reset();
}
}
else {
// Failed to initialize or test frame failed, so keep looking
reset();
}
}
}
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
{
PLENTRY entry = du->bufferList;
@@ -224,8 +314,6 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Decoding failed: %d", err);
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
+15 -9
View File
@@ -22,16 +22,19 @@ public:
virtual void renderFrame(SDL_UserEvent* event) override;
virtual void dropFrame(SDL_UserEvent* event) override;
virtual IFFmpegRenderer* getRenderer();
private:
bool
chooseDecoder(
StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width, int height,
AVCodec*& chosenDecoder,
const AVCodecHWConfig*& chosenHwConfig,
IFFmpegRenderer*& newRenderer);
bool completeInitialization(AVCodec* decoder, int videoFormat,
int width, int height, bool testOnly);
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
void reset();
static
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts);
AVPacket m_Pkt;
AVCodecContext* m_VideoDecoderCtx;
@@ -39,4 +42,7 @@ private:
const AVCodecHWConfig* m_HwDecodeCfg;
IFFmpegRenderer* m_Renderer;
SDL_atomic_t m_QueuedFrames;
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,
};
+1
View File
@@ -0,0 +1 @@
Versions/Current/Headers
+1
View File
@@ -0,0 +1 @@
Versions/Current/Resources
+1
View File
@@ -0,0 +1 @@
Versions/Current/SDL2

Some files were not shown because too many files have changed in this diff Show More