Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81943b12c0 | ||
|
|
18fa7c2832 | ||
|
|
dd02bf809e | ||
|
|
2f3a1a5d79 | ||
|
|
18654cb011 | ||
|
|
5954f80645 | ||
|
|
3e36357315 | ||
|
|
a06a454ade | ||
|
|
c0fab4b41f | ||
|
|
80b79f3636 | ||
|
|
b97ae25f93 | ||
|
|
4722ea6096 | ||
|
|
3e9781033a | ||
|
|
62b296cf85 | ||
|
|
dcbaa52683 | ||
|
|
1e29f61656 | ||
|
|
89319dc7aa | ||
|
|
5755afc4ea | ||
|
|
d00c434ccc | ||
|
|
4ada59ba48 | ||
|
|
14ccc38f4c | ||
|
|
a8be299bbd | ||
|
|
2e87f5ff85 | ||
|
|
829ebba622 | ||
|
|
81019fa2a8 | ||
|
|
1bc3675548 | ||
|
|
678ec979c9 | ||
|
|
d1a48ae641 | ||
|
|
eded86bc10 | ||
|
|
5a7a49d1d5 | ||
|
|
d3de8049bf | ||
|
|
d09dd90a73 | ||
|
|
78203eb9be | ||
|
|
31c7d4e36f | ||
|
|
d3949806f5 | ||
|
|
28ff15c99e | ||
|
|
bd2c74fe90 | ||
|
|
fb6995db94 | ||
|
|
8c2c4d3282 | ||
|
|
b81392b818 | ||
|
|
ad08440023 | ||
|
|
ec17623400 | ||
|
|
288eca7849 | ||
|
|
b5f202150e | ||
|
|
539bf0cb30 | ||
|
|
9385d62c89 | ||
|
|
897f916552 | ||
|
|
914cf6753e | ||
|
|
392208057e | ||
|
|
8d2cbf806d | ||
|
|
ab5025efbf | ||
|
|
912e51d863 | ||
|
|
d1c4ca5eae | ||
|
|
16bb4a148e | ||
|
|
d7071cb67f | ||
|
|
a13fb3d7c5 | ||
|
|
85558904a4 | ||
|
|
88ed72fdc1 | ||
|
|
586a93d7aa | ||
|
|
a446699c6c | ||
|
|
f1c459fd6c | ||
|
|
752f0a988b | ||
|
|
719b266691 | ||
|
|
dcba5762c7 | ||
|
|
0757717bea | ||
|
|
a9f762aa96 | ||
|
|
db45f46d9e | ||
|
|
f00653964e | ||
|
|
e0154f9314 | ||
|
|
4408a3a354 | ||
|
|
5abf5a157d | ||
|
|
d55d5f60a5 | ||
|
|
feacf2c359 | ||
|
|
72ea1601e9 | ||
|
|
02cfd8d978 | ||
|
|
728377cba3 | ||
|
|
f2283b56b9 | ||
|
|
b515f2a599 | ||
|
|
4354b49496 | ||
|
|
994312c212 | ||
|
|
5f25e0d4e2 | ||
|
|
4bbe571f5f | ||
|
|
c58d21375c | ||
|
|
ed128c558f | ||
|
|
828932d95c | ||
|
|
42ced309d3 | ||
|
|
1fcd306879 | ||
|
|
26c9ad7f06 | ||
|
|
4b7b01ce0a | ||
|
|
6632154906 | ||
|
|
ca67c7ef43 | ||
|
|
c4039a929c | ||
|
|
a1b74e1220 | ||
|
|
01f6188824 | ||
|
|
283f31a734 | ||
|
|
b800ce9ebe | ||
|
|
eb9d54465c | ||
|
|
eb9e1f81d4 | ||
|
|
ddfbd70b55 | ||
|
|
51e9ab83ad | ||
|
|
5ed7e03b2e | ||
|
|
0cb8d5b12f | ||
|
|
b4548c1af2 | ||
|
|
0a396f3112 | ||
|
|
2aa18dc88f | ||
|
|
8d89983eeb | ||
|
|
573c470965 | ||
|
|
daa0c751e2 | ||
|
|
e1babefc80 | ||
|
|
ce99b0e162 | ||
|
|
4c3a23b1bd | ||
|
|
12016af326 | ||
|
|
fc9871b075 | ||
|
|
3cb3c1c6d8 | ||
|
|
8fe3474589 | ||
|
|
dc3c565ec0 | ||
|
|
350c7d7081 | ||
|
|
0428bc8fe3 | ||
|
|
514a8575a2 | ||
|
|
fc11377552 | ||
|
|
fbaa70a2ae | ||
|
|
f30c11ddf4 | ||
|
|
51c4ecc879 | ||
|
|
080421f2fb | ||
|
|
8cc61ca8b8 | ||
|
|
400754c5b3 | ||
|
|
05e82c24f8 | ||
|
|
97c15c7876 | ||
|
|
36e7cc8e89 | ||
|
|
ad2ce4de53 | ||
|
|
2a28f7c4d2 | ||
|
|
0cdbe080a6 | ||
|
|
96fe1a7da1 | ||
|
|
4f73af2920 | ||
|
|
7ca4e49bf5 | ||
|
|
dfa63297fc | ||
|
|
402e8c1897 | ||
|
|
33e814122e | ||
|
|
f28a4df5ee | ||
|
|
417aa2815c | ||
|
|
2fdabe37aa | ||
|
|
d8270dfb6a | ||
|
|
b75f662c41 | ||
|
|
0d9d0845f5 | ||
|
|
5d66a1f298 | ||
|
|
9bf52eb63a | ||
|
|
0c9d72a9a1 | ||
|
|
c2c2bd2df6 | ||
|
|
78fc594d84 | ||
|
|
53b1e7c573 | ||
|
|
c14404a7c1 | ||
|
|
96e6750f9c | ||
|
|
dbafd05a4e | ||
|
|
d7fd578cc3 |
@@ -146,7 +146,7 @@ private:
|
|||||||
L"EZFRD32.dll",
|
L"EZFRD32.dll",
|
||||||
L"EZFRD64.dll",
|
L"EZFRD64.dll",
|
||||||
|
|
||||||
// These are the dList DLLs for Optimus hybrid graphics DDI.
|
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
|
||||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
||||||
//
|
//
|
||||||
// We forcefully block them from loading because Optimus has a bug that
|
// We forcefully block them from loading because Optimus has a bug that
|
||||||
@@ -157,6 +157,33 @@ private:
|
|||||||
L"nvdlist.dll",
|
L"nvdlist.dll",
|
||||||
L"nvdlistx.dll",
|
L"nvdlistx.dll",
|
||||||
|
|
||||||
|
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
|
||||||
|
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
||||||
|
//
|
||||||
|
// These seem to cause a crash in PresentEx() in full-screen exclusive mode.
|
||||||
|
// This block will prevent Optimus from ever using the dGPU even if the user has requested it.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/386
|
||||||
|
//
|
||||||
|
// d3d9!CSwapChain::BltToHybridPrimary+0x200:
|
||||||
|
// 00007ffa`23f37e58 488b01 mov rax,qword ptr [rcx] ds:00000000`00000038=????????????????
|
||||||
|
// 00 0000004e`496ff4e0 00007ffa`23f39e2c d3d9!CSwapChain::BltToHybridPrimary+0x200
|
||||||
|
// 01 0000004e`496ff880 00007ffa`23ee39ce d3d9!CSwapChain::FlipToSurface+0x15c
|
||||||
|
// 02 0000004e`496ff900 00007ffa`23f4dd75 d3d9!CSwapChain::PresentMain+0x3e13e
|
||||||
|
// 03 0000004e`496ffab0 00007ffa`23f4dccd d3d9!CBaseDevice::PresentMain+0x9d
|
||||||
|
// 04 0000004e`496ffb00 00007ff7`8e31016f d3d9!CBaseDevice::PresentEx+0xbd
|
||||||
|
// 05 0000004e`496ffb50 00007ff7`8e30df1e Moonlight!DXVA2Renderer::renderFrame+0x61f [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\dxva2.cpp @ 1035]
|
||||||
|
// 06 0000004e`496ffd50 00007ff7`8e30e46a Moonlight!Pacer::renderFrame+0x3e [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 265]
|
||||||
|
// 07 (Inline Function) --------`-------- Moonlight!Pacer::renderLastFrameAndUnlock+0x197 [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 156]
|
||||||
|
// 08 0000004e`496ffda0 00007ffa`14476978 Moonlight!Pacer::renderThread+0x1ca [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 88]
|
||||||
|
// 09 0000004e`496ffde0 00007ffa`14476ee2 SDL2!SDL_RunThread+0x38 [C:\Users\camer\SDL\src\thread\SDL_thread.c @ 276]
|
||||||
|
// 0a 0000004e`496ffe10 00007ffa`2aae0e82 SDL2!RunThread+0x12 [C:\Users\camer\SDL\src\thread\windows\SDL_systhread.c @ 83]
|
||||||
|
// 0b 0000004e`496ffe40 00007ffa`2d627bd4 ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
|
||||||
|
// 0c 0000004e`496ffe70 00007ffa`2da4ce51 kernel32!BaseThreadInitThunk+0x14
|
||||||
|
// 0d 0000004e`496ffea0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
|
||||||
|
//
|
||||||
|
L"nvinit.dll",
|
||||||
|
L"nvinitx.dll",
|
||||||
|
|
||||||
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
|
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
|
||||||
// code inside d3d9.dll that causes a crash when executed:
|
// code inside d3d9.dll that causes a crash when executed:
|
||||||
//
|
//
|
||||||
|
|||||||
+1
-1
Submodule app/SDL_GameControllerDB updated: b0486daf50...7518c26389
+23
-17
@@ -59,20 +59,6 @@ unix:!macx {
|
|||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||||
|
|
||||||
# SLAudio is used on Steam Link
|
|
||||||
!config_SL {
|
|
||||||
CONFIG += soundio
|
|
||||||
}
|
|
||||||
|
|
||||||
soundio {
|
|
||||||
packagesExist(libpulse) {
|
|
||||||
PKGCONFIG += libpulse
|
|
||||||
}
|
|
||||||
packagesExist(alsa) {
|
|
||||||
PKGCONFIG += alsa
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
packagesExist(libavcodec) {
|
||||||
PKGCONFIG += libavcodec libavutil
|
PKGCONFIG += libavcodec libavutil
|
||||||
CONFIG += ffmpeg
|
CONFIG += ffmpeg
|
||||||
@@ -133,6 +119,7 @@ macx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
backend/nvapp.cpp \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
backend/computerseeker.cpp \
|
backend/computerseeker.cpp \
|
||||||
backend/identitymanager.cpp \
|
backend/identitymanager.cpp \
|
||||||
@@ -146,7 +133,12 @@ SOURCES += \
|
|||||||
cli/quitstream.cpp \
|
cli/quitstream.cpp \
|
||||||
cli/startstream.cpp \
|
cli/startstream.cpp \
|
||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input.cpp \
|
streaming/input/abstouch.cpp \
|
||||||
|
streaming/input/gamepad.cpp \
|
||||||
|
streaming/input/input.cpp \
|
||||||
|
streaming/input/keyboard.cpp \
|
||||||
|
streaming/input/mouse.cpp \
|
||||||
|
streaming/input/reltouch.cpp \
|
||||||
streaming/session.cpp \
|
streaming/session.cpp \
|
||||||
streaming/audio/audio.cpp \
|
streaming/audio/audio.cpp \
|
||||||
streaming/audio/renderers/sdlaud.cpp \
|
streaming/audio/renderers/sdlaud.cpp \
|
||||||
@@ -162,6 +154,7 @@ SOURCES += \
|
|||||||
wm.cpp
|
wm.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
backend/nvapp.h \
|
||||||
utils.h \
|
utils.h \
|
||||||
backend/computerseeker.h \
|
backend/computerseeker.h \
|
||||||
backend/identitymanager.h \
|
backend/identitymanager.h \
|
||||||
@@ -175,7 +168,7 @@ HEADERS += \
|
|||||||
cli/quitstream.h \
|
cli/quitstream.h \
|
||||||
cli/startstream.h \
|
cli/startstream.h \
|
||||||
settings/streamingpreferences.h \
|
settings/streamingpreferences.h \
|
||||||
streaming/input.h \
|
streaming/input/input.h \
|
||||||
streaming/session.h \
|
streaming/session.h \
|
||||||
streaming/audio/renderers/renderer.h \
|
streaming/audio/renderers/renderer.h \
|
||||||
streaming/audio/renderers/sdl.h \
|
streaming/audio/renderers/sdl.h \
|
||||||
@@ -257,6 +250,16 @@ libdrm {
|
|||||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||||
}
|
}
|
||||||
|
config_EGL {
|
||||||
|
message(EGL renderer selected)
|
||||||
|
|
||||||
|
CONFIG += egl
|
||||||
|
DEFINES += HAVE_EGL
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/egl_extensions.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
|
||||||
|
}
|
||||||
config_SL {
|
config_SL {
|
||||||
message(Steam Link build configuration selected)
|
message(Steam Link build configuration selected)
|
||||||
|
|
||||||
@@ -398,7 +401,10 @@ macx {
|
|||||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||||
|
|
||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||||
|
APP_BUNDLE_PLIST.path = Contents
|
||||||
|
|
||||||
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
|
||||||
|
|
||||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,12 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
void AutoUpdateChecker::start()
|
void AutoUpdateChecker::start()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 14, 2) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
|
QT_WARNING_PUSH
|
||||||
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
|
|||||||
@@ -87,6 +87,16 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
return QUrl("qrc:/res/no_app_image.png");
|
return QUrl("qrc:/res/no_app_image.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BoxArtManager::deleteBoxArt(NvComputer* computer)
|
||||||
|
{
|
||||||
|
QDir dir(Path::getBoxArtCacheDir());
|
||||||
|
|
||||||
|
// Delete everything in this computer's box art directory
|
||||||
|
if (dir.cd(computer->uuid)) {
|
||||||
|
dir.removeRecursively();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
||||||
{
|
{
|
||||||
if (!image.isEmpty()) {
|
if (!image.isEmpty()) {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public:
|
|||||||
QUrl
|
QUrl
|
||||||
loadBoxArt(NvComputer* computer, NvApp& app);
|
loadBoxArt(NvComputer* computer, NvApp& app);
|
||||||
|
|
||||||
|
static
|
||||||
|
void
|
||||||
|
deleteBoxArt(NvComputer* computer);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void
|
void
|
||||||
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "computermanager.h"
|
#include "computermanager.h"
|
||||||
|
#include "boxartmanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
@@ -67,12 +68,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
QWriteLocker lock(&m_Computer->lock);
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
if (m_Computer->appList != appList) {
|
changed = m_Computer->updateAppList(appList);
|
||||||
m_Computer->appList = appList;
|
|
||||||
m_Computer->sortAppList();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +395,9 @@ public:
|
|||||||
// Delete the polling entry first. This will stop all polling threads too.
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
delete pollingEntry;
|
delete pollingEntry;
|
||||||
|
|
||||||
|
// Delete cached box art
|
||||||
|
BoxArtManager::deleteBoxArt(m_Computer);
|
||||||
|
|
||||||
// Finally, delete the computer itself. This must be done
|
// Finally, delete the computer itself. This must be done
|
||||||
// last because the polling thread might be using it.
|
// last because the polling thread might be using it.
|
||||||
delete m_Computer;
|
delete m_Computer;
|
||||||
@@ -416,6 +415,28 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QWriteLocker(&computer->lock);
|
||||||
|
|
||||||
|
computer->name = name;
|
||||||
|
computer->hasCustomName = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the UI of the state change
|
||||||
|
handleComputerStateChanged(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||||
|
{
|
||||||
|
// Persist the change
|
||||||
|
saveHosts();
|
||||||
|
|
||||||
|
// Notify the UI of the state change
|
||||||
|
handleComputerStateChanged(computer);
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleAboutToQuit()
|
void ComputerManager::handleAboutToQuit()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_Lock);
|
QWriteLocker lock(&m_Lock);
|
||||||
@@ -586,7 +607,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||||
|
|
||||||
void computerStateChanged(NvComputer* computer);
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
@@ -616,7 +637,20 @@ private:
|
|||||||
return serverInfo;
|
return serverInfo;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(false);
|
StreamingPreferences prefs;
|
||||||
|
int portTestResult;
|
||||||
|
|
||||||
|
if (prefs.detectNetworkBlocking) {
|
||||||
|
// We failed to connect to the specified PC. Let's test to make sure this network
|
||||||
|
// isn't blocking Moonlight, so we can tell the user about it.
|
||||||
|
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
||||||
|
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
portTestResult = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
|
||||||
}
|
}
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
@@ -694,6 +728,12 @@ private:
|
|||||||
newComputer->manualAddress = m_Address;
|
newComputer->manualAddress = m_Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QHostAddress hostAddress(m_Address);
|
||||||
|
bool addressIsSiteLocalV4 =
|
||||||
|
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
||||||
|
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
||||||
|
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||||
|
|
||||||
{
|
{
|
||||||
// Check if this PC already exists
|
// Check if this PC already exists
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
@@ -708,7 +748,7 @@ private:
|
|||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
// For non-mDNS clients, let them know it succeeded
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(true);
|
emit computerAddCompleted(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell our client if something changed
|
// Tell our client if something changed
|
||||||
@@ -727,9 +767,24 @@ private:
|
|||||||
// Drop the lock before notifying
|
// Drop the lock before notifying
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|
||||||
|
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||||
|
// go ahead and do the STUN request now to populate an external address.
|
||||||
|
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
||||||
|
quint32 addr;
|
||||||
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
|
if (err == 0) {
|
||||||
|
lock.relock();
|
||||||
|
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "STUN failed to get WAN address:" << err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
// For non-mDNS clients, let them know it succeeded
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(true);
|
emit computerAddCompleted(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell our client about this new PC
|
// Tell our client about this new PC
|
||||||
|
|||||||
@@ -179,12 +179,16 @@ public:
|
|||||||
// computer is deleted inside this call
|
// computer is deleted inside this call
|
||||||
void deleteHost(NvComputer* computer);
|
void deleteHost(NvComputer* computer);
|
||||||
|
|
||||||
|
void renameHost(NvComputer* computer, QString name);
|
||||||
|
|
||||||
|
void clientSideAttributeUpdated(NvComputer* computer);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void computerStateChanged(NvComputer* computer);
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
void pairingCompleted(NvComputer* computer, QString error);
|
void pairingCompleted(NvComputer* computer, QString error);
|
||||||
|
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||||
|
|
||||||
void quitAppCompleted(QVariant error);
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/rsa.h>
|
#include <openssl/rsa.h>
|
||||||
#include <openssl/bn.h>
|
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
@@ -32,20 +31,19 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
X509* cert = X509_new();
|
X509* cert = X509_new();
|
||||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||||
|
|
||||||
EVP_PKEY* pk = EVP_PKEY_new();
|
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||||
|
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||||
|
|
||||||
|
EVP_PKEY_keygen_init(ctx);
|
||||||
|
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
|
||||||
|
|
||||||
|
// pk must be initialized on input
|
||||||
|
EVP_PKEY* pk = NULL;
|
||||||
|
EVP_PKEY_keygen(ctx, &pk);
|
||||||
|
|
||||||
|
EVP_PKEY_CTX_free(ctx);
|
||||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||||
|
|
||||||
BIGNUM* bne = BN_new();
|
|
||||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
|
||||||
|
|
||||||
RSA* rsa = RSA_new();
|
|
||||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
|
||||||
|
|
||||||
BN_set_word(bne, RSA_F4);
|
|
||||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
|
||||||
|
|
||||||
EVP_PKEY_assign_RSA(pk, rsa);
|
|
||||||
|
|
||||||
X509_set_version(cert, 2);
|
X509_set_version(cert, 2);
|
||||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||||
@@ -94,7 +92,6 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
|
|
||||||
X509_free(cert);
|
X509_free(cert);
|
||||||
EVP_PKEY_free(pk);
|
EVP_PKEY_free(pk);
|
||||||
BN_free(bne);
|
|
||||||
BIO_free(biokey);
|
BIO_free(biokey);
|
||||||
BIO_free(biocert);
|
BIO_free(biocert);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#include "nvapp.h"
|
||||||
|
|
||||||
|
#define SER_APPNAME "name"
|
||||||
|
#define SER_APPID "id"
|
||||||
|
#define SER_APPHDR "hdr"
|
||||||
|
#define SER_APPCOLLECTOR "appcollector"
|
||||||
|
#define SER_HIDDEN "hidden"
|
||||||
|
|
||||||
|
NvApp::NvApp(QSettings& settings)
|
||||||
|
{
|
||||||
|
name = settings.value(SER_APPNAME).toString();
|
||||||
|
id = settings.value(SER_APPID).toInt();
|
||||||
|
hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||||
|
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
|
||||||
|
hidden = settings.value(SER_HIDDEN).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvApp::serialize(QSettings& settings) const
|
||||||
|
{
|
||||||
|
settings.setValue(SER_APPNAME, name);
|
||||||
|
settings.setValue(SER_APPID, id);
|
||||||
|
settings.setValue(SER_APPHDR, hdrSupported);
|
||||||
|
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
|
||||||
|
settings.setValue(SER_HIDDEN, hidden);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
class NvApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NvApp() {}
|
||||||
|
explicit NvApp(QSettings& settings);
|
||||||
|
|
||||||
|
bool operator==(const NvApp& other) const
|
||||||
|
{
|
||||||
|
return id == other.id &&
|
||||||
|
name == other.name &&
|
||||||
|
hdrSupported == other.hdrSupported &&
|
||||||
|
isAppCollectorGame == other.isAppCollectorGame &&
|
||||||
|
hidden == other.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const NvApp& other) const
|
||||||
|
{
|
||||||
|
return !operator==(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInitialized()
|
||||||
|
{
|
||||||
|
return id != 0 && !name.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
serialize(QSettings& settings) const;
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
QString name;
|
||||||
|
bool hdrSupported = false;
|
||||||
|
bool isAppCollectorGame = false;
|
||||||
|
bool hidden = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(NvApp)
|
||||||
+37
-16
@@ -1,4 +1,5 @@
|
|||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
|
#include "nvapp.h"
|
||||||
|
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
@@ -14,15 +15,13 @@
|
|||||||
#define SER_IPV6ADDR "ipv6address"
|
#define SER_IPV6ADDR "ipv6address"
|
||||||
#define SER_APPLIST "apps"
|
#define SER_APPLIST "apps"
|
||||||
#define SER_SRVCERT "srvcert"
|
#define SER_SRVCERT "srvcert"
|
||||||
|
#define SER_CUSTOMNAME "customname"
|
||||||
#define SER_APPNAME "name"
|
|
||||||
#define SER_APPID "id"
|
|
||||||
#define SER_APPHDR "hdr"
|
|
||||||
|
|
||||||
NvComputer::NvComputer(QSettings& settings)
|
NvComputer::NvComputer(QSettings& settings)
|
||||||
{
|
{
|
||||||
this->name = settings.value(SER_NAME).toString();
|
this->name = settings.value(SER_NAME).toString();
|
||||||
this->uuid = settings.value(SER_UUID).toString();
|
this->uuid = settings.value(SER_UUID).toString();
|
||||||
|
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
|
||||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||||
@@ -32,14 +31,9 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
|
|
||||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appCount; i++) {
|
for (int i = 0; i < appCount; i++) {
|
||||||
NvApp app;
|
|
||||||
|
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
app.name = settings.value(SER_APPNAME).toString();
|
NvApp app(settings);
|
||||||
app.id = settings.value(SER_APPID).toInt();
|
|
||||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
|
||||||
|
|
||||||
this->appList.append(app);
|
this->appList.append(app);
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
@@ -62,6 +56,7 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
QReadLocker lock(&this->lock);
|
QReadLocker lock(&this->lock);
|
||||||
|
|
||||||
settings.setValue(SER_NAME, name);
|
settings.setValue(SER_NAME, name);
|
||||||
|
settings.setValue(SER_CUSTOMNAME, hasCustomName);
|
||||||
settings.setValue(SER_UUID, uuid);
|
settings.setValue(SER_UUID, uuid);
|
||||||
settings.setValue(SER_MAC, macAddress);
|
settings.setValue(SER_MAC, macAddress);
|
||||||
settings.setValue(SER_LOCALADDR, localAddress);
|
settings.setValue(SER_LOCALADDR, localAddress);
|
||||||
@@ -76,10 +71,7 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
settings.beginWriteArray(SER_APPLIST);
|
settings.beginWriteArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appList.count(); i++) {
|
for (int i = 0; i < appList.count(); i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
|
appList[i].serialize(settings);
|
||||||
settings.setValue(SER_APPNAME, appList[i].name);
|
|
||||||
settings.setValue(SER_APPID, appList[i].id);
|
|
||||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
}
|
}
|
||||||
@@ -96,6 +88,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
{
|
{
|
||||||
this->serverCert = serverCert;
|
this->serverCert = serverCert;
|
||||||
|
|
||||||
|
this->hasCustomName = false;
|
||||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||||
if (this->name.isEmpty()) {
|
if (this->name.isEmpty()) {
|
||||||
this->name = "UNKNOWN";
|
this->name = "UNKNOWN";
|
||||||
@@ -164,8 +157,9 @@ bool NvComputer::wake()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
const quint16 WOL_PORTS[] = {
|
||||||
7, 9, // Standard WOL ports
|
9, // Standard WOL port (privileged port)
|
||||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the WoL payload
|
// Create the WoL payload
|
||||||
@@ -313,6 +307,25 @@ bool NvComputer::isReachableOverVpn()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||||
|
if (appList == newAppList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate client-side attributes to the new app list
|
||||||
|
for (const NvApp& existingApp : appList) {
|
||||||
|
for (NvApp& newApp : newAppList) {
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
newApp.hidden = existingApp.hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appList = newAppList;
|
||||||
|
sortAppList();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QVector<QString> NvComputer::uniqueAddresses() const
|
QVector<QString> NvComputer::uniqueAddresses() const
|
||||||
{
|
{
|
||||||
QVector<QString> uniqueAddressList;
|
QVector<QString> uniqueAddressList;
|
||||||
@@ -377,7 +390,10 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
changed = true; \
|
changed = true; \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasCustomName) {
|
||||||
|
// Only overwrite the name if it's not custom
|
||||||
ASSIGN_IF_CHANGED(name);
|
ASSIGN_IF_CHANGED(name);
|
||||||
|
}
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||||
@@ -393,7 +409,12 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
ASSIGN_IF_CHANGED(gpuModel);
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
|
|
||||||
|
if (!that.appList.isEmpty()) {
|
||||||
|
// updateAppList() handles merging client-side attributes
|
||||||
|
updateAppList(that.appList);
|
||||||
|
}
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class NvComputer
|
|||||||
private:
|
private:
|
||||||
void sortAppList();
|
void sortAppList();
|
||||||
|
|
||||||
|
bool updateAppList(QVector<NvApp> newAppList);
|
||||||
|
|
||||||
bool pendingQuit;
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -71,6 +73,7 @@ public:
|
|||||||
QString manualAddress;
|
QString manualAddress;
|
||||||
QByteArray macAddress;
|
QByteArray macAddress;
|
||||||
QString name;
|
QString name;
|
||||||
|
bool hasCustomName;
|
||||||
QString uuid;
|
QString uuid;
|
||||||
QSslCertificate serverCert;
|
QSslCertificate serverCert;
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
|
|||||||
+10
-3
@@ -163,7 +163,11 @@ NvHTTP::launchApp(int appId,
|
|||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
QString::number(streamConfig->fps)+
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
|
// so force it to 0 to ensure the correct resolution is set. We
|
||||||
|
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||||
|
// on GFE 3.20.3.
|
||||||
|
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
@@ -440,9 +444,12 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Add our client certificate
|
// Add our client certificate
|
||||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 14, 2) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
|
QT_WARNING_PUSH
|
||||||
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(request);
|
QNetworkReply* reply = m_Nam.get(request);
|
||||||
|
|||||||
+1
-21
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "identitymanager.h"
|
#include "identitymanager.h"
|
||||||
|
#include "nvapp.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -8,27 +9,6 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
class NvApp
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
bool operator==(const NvApp& other) const
|
|
||||||
{
|
|
||||||
return id == other.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isInitialized()
|
|
||||||
{
|
|
||||||
return id != 0 && !name.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
int id;
|
|
||||||
QString name;
|
|
||||||
bool hdrSupported;
|
|
||||||
bool isAppCollectorGame;
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(NvApp)
|
|
||||||
|
|
||||||
class NvDisplayMode
|
class NvDisplayMode
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <openssl/bio.h>
|
#include <openssl/bio.h>
|
||||||
#include <openssl/aes.h>
|
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
@@ -51,39 +52,63 @@ NvPairingManager::generateRandomBytes(int length)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
|
||||||
{
|
{
|
||||||
QByteArray ciphertext(plaintext.size(), 0);
|
QByteArray ciphertext(plaintext.size(), 0);
|
||||||
|
EVP_CIPHER_CTX* cipher;
|
||||||
|
int ciphertextLen;
|
||||||
|
|
||||||
for (int i = 0; i < plaintext.size(); i += 16)
|
cipher = EVP_CIPHER_CTX_new();
|
||||||
{
|
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
|
||||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||||
key);
|
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||||
}
|
|
||||||
|
EVP_EncryptUpdate(cipher,
|
||||||
|
reinterpret_cast<unsigned char*>(ciphertext.data()),
|
||||||
|
&ciphertextLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(plaintext.data()),
|
||||||
|
plaintext.length());
|
||||||
|
Q_ASSERT(ciphertextLen == ciphertext.length());
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_free(cipher);
|
||||||
|
|
||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||||
{
|
{
|
||||||
QByteArray plaintext(ciphertext.size(), 0);
|
QByteArray plaintext(ciphertext.size(), 0);
|
||||||
|
EVP_CIPHER_CTX* cipher;
|
||||||
|
int plaintextLen;
|
||||||
|
|
||||||
for (int i = 0; i < plaintext.size(); i += 16)
|
cipher = EVP_CIPHER_CTX_new();
|
||||||
{
|
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
|
||||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||||
key);
|
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||||
}
|
|
||||||
|
EVP_DecryptUpdate(cipher,
|
||||||
|
reinterpret_cast<unsigned char*>(plaintext.data()),
|
||||||
|
&plaintextLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(ciphertext.data()),
|
||||||
|
ciphertext.length());
|
||||||
|
Q_ASSERT(plaintextLen == plaintext.length());
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_free(cipher);
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||||
{
|
{
|
||||||
|
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
|
||||||
|
#else
|
||||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||||
|
#endif
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
@@ -107,9 +132,13 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
|
||||||
{
|
{
|
||||||
|
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
|
||||||
|
#else
|
||||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||||
|
#endif
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
@@ -123,7 +152,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
|||||||
|
|
||||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
|
||||||
|
|
||||||
EVP_PKEY_free(pubKey);
|
EVP_PKEY_free(pubKey);
|
||||||
EVP_MD_CTX_destroy(mdctx);
|
EVP_MD_CTX_destroy(mdctx);
|
||||||
@@ -133,7 +162,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::signMessage(QByteArray message)
|
NvPairingManager::signMessage(const QByteArray& message)
|
||||||
{
|
{
|
||||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||||
@@ -143,7 +172,7 @@ NvPairingManager::signMessage(QByteArray message)
|
|||||||
|
|
||||||
EVP_DigestInit_ex(ctx, md, NULL);
|
EVP_DigestInit_ex(ctx, md, NULL);
|
||||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||||
|
|
||||||
size_t signatureLength = 0;
|
size_t signatureLength = 0;
|
||||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||||
@@ -157,7 +186,7 @@ NvPairingManager::signMessage(QByteArray message)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||||
{
|
{
|
||||||
return QByteArray().append(salt).append(pin.toLatin1());
|
return QByteArray().append(salt).append(pin.toLatin1());
|
||||||
}
|
}
|
||||||
@@ -186,9 +215,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
QByteArray salt = generateRandomBytes(16);
|
QByteArray salt = generateRandomBytes(16);
|
||||||
QByteArray saltedPin = saltPin(salt, pin);
|
QByteArray saltedPin = saltPin(salt, pin);
|
||||||
|
|
||||||
AES_KEY encKey, decKey;
|
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
aesKey.truncate(16);
|
||||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
|
||||||
|
|
||||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
@@ -223,7 +251,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
m_Http.setServerCert(serverCert);
|
m_Http.setServerCert(serverCert);
|
||||||
|
|
||||||
QByteArray randomChallenge = generateRandomBytes(16);
|
QByteArray randomChallenge = generateRandomBytes(16);
|
||||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientchallenge=" +
|
"devicename=roth&updateState=1&clientchallenge=" +
|
||||||
@@ -237,7 +265,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
|
||||||
QByteArray clientSecretData = generateRandomBytes(16);
|
QByteArray clientSecretData = generateRandomBytes(16);
|
||||||
QByteArray challengeResponse;
|
QByteArray challengeResponse;
|
||||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||||
@@ -258,7 +286,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
|
|
||||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||||
paddedHash.resize(32);
|
paddedHash.resize(32);
|
||||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
|
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
|
||||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "identitymanager.h"
|
#include "identitymanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
|
||||||
#include <openssl/aes.h>
|
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
@@ -30,22 +29,22 @@ private:
|
|||||||
generateRandomBytes(int length);
|
generateRandomBytes(int length);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
saltPin(QByteArray salt, QString pin);
|
saltPin(const QByteArray& salt, QString pin);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
encrypt(const QByteArray& plaintext, const QByteArray& key);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
getSignatureFromPemCert(QByteArray certificate);
|
getSignatureFromPemCert(const QByteArray& certificate);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
signMessage(QByteArray message);
|
signMessage(const QByteArray& message);
|
||||||
|
|
||||||
NvHTTP m_Http;
|
NvHTTP m_Http;
|
||||||
X509* m_Cert;
|
X509* m_Cert;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
SystemProperties::SystemProperties()
|
SystemProperties::SystemProperties()
|
||||||
{
|
{
|
||||||
|
versionString = QString(VERSION_STR);
|
||||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||||
isRunningWayland = WMUtils::isRunningWayland();
|
isRunningWayland = WMUtils::isRunningWayland();
|
||||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||||
@@ -119,7 +120,14 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||||
|
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create test window with platform flags: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
if (!testWindow) {
|
if (!testWindow) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to create window for hardware decode test: %s",
|
"Failed to create window for hardware decode test: %s",
|
||||||
@@ -127,6 +135,7 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public:
|
|||||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
||||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||||
|
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
@@ -44,5 +45,6 @@ private:
|
|||||||
QSize maximumResolution;
|
QSize maximumResolution;
|
||||||
QList<QRect> monitorDesktopResolutions;
|
QList<QRect> monitorDesktopResolutions;
|
||||||
QList<QRect> monitorNativeResolutions;
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
QString versionString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -261,8 +261,8 @@ StreamCommandLineParser::StreamCommandLineParser()
|
|||||||
};
|
};
|
||||||
m_VideoDecoderMap = {
|
m_VideoDecoderMap = {
|
||||||
{"auto", StreamingPreferences::VDS_AUTO},
|
{"auto", StreamingPreferences::VDS_AUTO},
|
||||||
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||||
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +297,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||||
parser.addToggleOption("quit-after", "quit app after session");
|
parser.addToggleOption("quit-after", "quit app after session");
|
||||||
parser.addToggleOption("absolute-mouse", "enable direct mouse control (best for remote desktop rather than games)");
|
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||||
|
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||||
|
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||||
parser.addToggleOption("game-optimization", "game optimizations");
|
parser.addToggleOption("game-optimization", "game optimizations");
|
||||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||||
@@ -388,6 +390,12 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --absolute-mouse and --no-absolute-mouse options
|
// Resolve --absolute-mouse and --no-absolute-mouse options
|
||||||
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
||||||
|
|
||||||
|
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
|
||||||
|
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
|
||||||
|
|
||||||
|
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
|
||||||
|
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
|
||||||
|
|
||||||
// Resolve --game-optimization and --no-game-optimization options
|
// Resolve --game-optimization and --no-game-optimization options
|
||||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
|
||||||
<p>Features include:</p>
|
<p>Features include:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
<li>Streaming at up to 4K resolution</li>
|
||||||
@@ -35,6 +35,52 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.2.0" date="2020-09-10">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added the ability to hide unwanted apps and games</li>
|
||||||
|
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
|
||||||
|
<li>Added the option to swap mouse buttons</li>
|
||||||
|
<li>Added codec and resolution to the on-screen performance stats</li>
|
||||||
|
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
|
||||||
|
<li>Added a Discord link to the toolbar in settings</li>
|
||||||
|
<li>Enabled the new EGL renderer by default on Linux</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming over networks that block QoS-tagged packets</li>
|
||||||
|
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
|
||||||
|
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
|
||||||
|
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
|
||||||
|
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
|
||||||
|
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
|
||||||
|
<li>Fixed several CLI options not properly overriding GUI options</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated to Qt 5.15.1</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.1.0" date="2020-05-17">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
|
||||||
|
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
|
||||||
|
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
|
||||||
|
<li>Added the option to rename host PCs</li>
|
||||||
|
<li>Added the ability to scroll the settings page with the right analog stick</li>
|
||||||
|
<li>Various UI improvements</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed unreliable mouse capture when the stream begins</li>
|
||||||
|
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
|
||||||
|
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="2.0.0" date="2020-04-28">
|
<release version="2.0.0" date="2020-04-28">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
|
|||||||
+35
-17
@@ -9,6 +9,7 @@ CenteredGridView {
|
|||||||
property int computerIndex
|
property int computerIndex
|
||||||
property AppModel appModel : createModel()
|
property AppModel appModel : createModel()
|
||||||
property bool activated
|
property bool activated
|
||||||
|
property bool showHiddenGames
|
||||||
|
|
||||||
id: appGrid
|
id: appGrid
|
||||||
focus: true
|
focus: true
|
||||||
@@ -48,7 +49,7 @@ CenteredGridView {
|
|||||||
function createModel()
|
function createModel()
|
||||||
{
|
{
|
||||||
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
||||||
model.initialize(ComputerManager, computerIndex)
|
model.initialize(ComputerManager, computerIndex, showHiddenGames)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,9 @@ CenteredGridView {
|
|||||||
width: 220; height: 287;
|
width: 220; height: 287;
|
||||||
grid: appGrid
|
grid: appGrid
|
||||||
|
|
||||||
|
// Dim the app if it's hidden
|
||||||
|
opacity: model.hidden ? 0.4 : 1.0
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
property bool isPlaceholder: false
|
property bool isPlaceholder: false
|
||||||
|
|
||||||
@@ -165,8 +169,8 @@ CenteredGridView {
|
|||||||
|
|
||||||
function launchOrResumeSelectedApp()
|
function launchOrResumeSelectedApp()
|
||||||
{
|
{
|
||||||
var runningIndex = appModel.getRunningAppIndex()
|
var runningId = appModel.getRunningAppId()
|
||||||
if (runningIndex >= 0 && runningIndex !== index) {
|
if (runningId !== 0 && runningId !== model.appid) {
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
quitAppDialog.segueToStream = true
|
quitAppDialog.segueToStream = true
|
||||||
quitAppDialog.nextAppName = model.name
|
quitAppDialog.nextAppName = model.name
|
||||||
@@ -190,6 +194,25 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressAndHold: {
|
||||||
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
|
if (appContextMenu.popup) {
|
||||||
|
appContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton;
|
||||||
|
onClicked: {
|
||||||
|
parent.onPressAndHold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
// Open the app context menu if activated via the gamepad or keyboard
|
// Open the app context menu if activated via the gamepad or keyboard
|
||||||
// for running games. If the game isn't running, the above onClicked
|
// for running games. If the game isn't running, the above onClicked
|
||||||
@@ -228,6 +251,14 @@ CenteredGridView {
|
|||||||
onTriggered: doQuitGame()
|
onTriggered: doQuitGame()
|
||||||
visible: model.running
|
visible: model.running
|
||||||
}
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: appContextMenu
|
||||||
|
checkable: true
|
||||||
|
checked: model.hidden
|
||||||
|
text: "Hide Game"
|
||||||
|
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||||
|
visible: !model.running || model.hidden
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,18 +294,5 @@ CenteredGridView {
|
|||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {}
|
||||||
// Manually hide the scrollbar to prevent it from being drawn on top
|
|
||||||
// of the StreamSegue during the transition. It can sometimes get stuck
|
|
||||||
// there since we're not consistently pumping the event loop while
|
|
||||||
// starting the stream.
|
|
||||||
visible: activated
|
|
||||||
|
|
||||||
parent: appGrid.parent
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
right: parent.right
|
|
||||||
bottom: parent.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import SdlGamepadKeyNavigation 1.0
|
|||||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
ComboBox {
|
ComboBox {
|
||||||
property int textWidth
|
property int textWidth
|
||||||
|
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
|
||||||
|
property int maximumWidth : parent.width
|
||||||
|
|
||||||
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
|
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: popupMetrics
|
id: popupMetrics
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
@@ -35,6 +36,11 @@ Item {
|
|||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
if (!launcher.isExecuted()) {
|
if (!launcher.isExecuted()) {
|
||||||
toolBar.visible = false
|
toolBar.visible = false
|
||||||
|
|
||||||
|
// Normally this is enabled by PcView, but we will won't
|
||||||
|
// load PcView when streaming from the command-line.
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
launcher.searchingComputer.connect(onSearchingComputer)
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
launcher.searchingApp.connect(onSearchingApp)
|
launcher.searchingApp.connect(onSearchingApp)
|
||||||
launcher.sessionCreated.connect(onSessionCreated)
|
launcher.sessionCreated.connect(onSessionCreated)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ NavigableDialog {
|
|||||||
id: dialog
|
id: dialog
|
||||||
|
|
||||||
property alias text: dialogLabel.dialogText
|
property alias text: dialogLabel.dialogText
|
||||||
|
property alias showSpinner: dialogSpinner.visible
|
||||||
|
property alias imageSrc: dialogImage.source
|
||||||
|
|
||||||
property string helpText
|
property string helpText
|
||||||
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
||||||
@@ -19,7 +21,13 @@ NavigableDialog {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: dialogSpinner
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
id: dialogImage
|
||||||
source: (standardButtons & Dialog.Yes) ?
|
source: (standardButtons & Dialog.Yes) ?
|
||||||
"qrc:/res/baseline-help_outline-24px.svg" :
|
"qrc:/res/baseline-help_outline-24px.svg" :
|
||||||
"qrc:/res/baseline-error_outline-24px.svg"
|
"qrc:/res/baseline-error_outline-24px.svg"
|
||||||
@@ -28,6 +36,7 @@ NavigableDialog {
|
|||||||
width: 50
|
width: 50
|
||||||
height: 50
|
height: 50
|
||||||
}
|
}
|
||||||
|
visible: !showSpinner
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|||||||
+128
-12
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
@@ -25,6 +26,9 @@ CenteredGridView {
|
|||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Any initialization done here that is critical for streaming must
|
||||||
|
// also be done in CliStartStreamSegue.qml, since this code does not run
|
||||||
|
// for command-line initiated streams.
|
||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
@@ -57,11 +61,18 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addComplete(success)
|
function addComplete(success, detectedPortBlocking)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC."
|
errorDialog.text = "Unable to connect to the specified PC."
|
||||||
|
|
||||||
|
if (detectedPortBlocking) {
|
||||||
|
errorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
|
||||||
|
}
|
||||||
|
else {
|
||||||
errorDialog.helpText = "Click the Help button for possible solutions."
|
errorDialog.helpText = "Click the Help button for possible solutions."
|
||||||
|
}
|
||||||
|
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +82,7 @@ CenteredGridView {
|
|||||||
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
||||||
model.initialize(ComputerManager)
|
model.initialize(ComputerManager)
|
||||||
model.pairingCompleted.connect(pairingComplete)
|
model.pairingCompleted.connect(pairingComplete)
|
||||||
|
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,12 +164,23 @@ CenteredGridView {
|
|||||||
id: pcContextMenu
|
id: pcContextMenu
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
parentMenu: pcContextMenu
|
parentMenu: pcContextMenu
|
||||||
text: "Delete PC"
|
text: "View Apps"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
var component = Qt.createComponent("AppView.qml")
|
||||||
// get confirmation first, actual closing is called from the dialog
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||||
deletePcDialog.open()
|
stackView.push(appView)
|
||||||
}
|
}
|
||||||
|
visible: model.online && model.paired
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "View Hidden Apps"
|
||||||
|
onTriggered: {
|
||||||
|
var component = Qt.createComponent("AppView.qml")
|
||||||
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||||
|
stackView.push(appView)
|
||||||
|
}
|
||||||
|
visible: model.online && model.paired
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
parentMenu: pcContextMenu
|
parentMenu: pcContextMenu
|
||||||
@@ -165,6 +188,33 @@ CenteredGridView {
|
|||||||
onTriggered: computerModel.wakeComputer(index)
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
visible: !model.online && model.wakeable
|
visible: !model.online && model.wakeable
|
||||||
}
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "Test Network"
|
||||||
|
onTriggered: {
|
||||||
|
computerModel.testConnectionForComputer(index)
|
||||||
|
testConnectionDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "Rename PC"
|
||||||
|
onTriggered: {
|
||||||
|
renamePcDialog.pcIndex = index
|
||||||
|
renamePcDialog.originalName = model.name
|
||||||
|
renamePcDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "Delete PC"
|
||||||
|
onTriggered: {
|
||||||
|
deletePcDialog.pcIndex = index
|
||||||
|
// get confirmation first, actual closing is called from the dialog
|
||||||
|
deletePcDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@@ -177,7 +227,7 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!model.busy) {
|
if (!model.busy) {
|
||||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
var pin = computerModel.generatePinString()
|
||||||
|
|
||||||
// Kick off pairing in the background
|
// Kick off pairing in the background
|
||||||
computerModel.pairComputer(index, pin)
|
computerModel.pairComputer(index, pin)
|
||||||
@@ -265,12 +315,78 @@ CenteredGridView {
|
|||||||
onAccepted: deletePc()
|
onAccepted: deletePc()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
NavigableMessageDialog {
|
||||||
parent: pcGrid.parent
|
id: testConnectionDialog
|
||||||
anchors {
|
closePolicy: Popup.CloseOnEscape
|
||||||
top: parent.top
|
standardButtons: Dialog.Ok
|
||||||
right: parent.right
|
|
||||||
bottom: parent.bottom
|
onAboutToShow: {
|
||||||
|
testConnectionDialog.text = "Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.\n\nThis may take a few seconds…"
|
||||||
|
showSpinner = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectionTestComplete(result, blockedPorts)
|
||||||
|
{
|
||||||
|
if (result === -1) {
|
||||||
|
text = "The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later."
|
||||||
|
imageSrc = "qrc:/res/baseline-warning-24px.svg"
|
||||||
|
}
|
||||||
|
else if (result === 0) {
|
||||||
|
text = "This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nIf you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection."
|
||||||
|
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = "Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.\n\nThe following network ports were blocked:\n"
|
||||||
|
text += blockedPorts
|
||||||
|
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop showing the spinner and show the image instead
|
||||||
|
showSpinner = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: renamePcDialog
|
||||||
|
property string label: "Enter the new name for this PC:"
|
||||||
|
property string originalName
|
||||||
|
property int pcIndex : -1;
|
||||||
|
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
editText.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
editText.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (editText.text) {
|
||||||
|
computerModel.renameComputer(pcIndex, editText.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Label {
|
||||||
|
text: renamePcDialog.label
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: editText
|
||||||
|
placeholderText: renamePcDialog.originalName
|
||||||
|
Layout.fillWidth: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
renamePcDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {}
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-20
@@ -16,12 +16,8 @@ Flickable {
|
|||||||
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
parent: settingsPage.parent
|
|
||||||
anchors {
|
anchors {
|
||||||
top: settingsPage.top
|
left: parent.right
|
||||||
left: settingsPage.right
|
|
||||||
bottom: settingsPage.bottom
|
|
||||||
|
|
||||||
leftMargin: -10
|
leftMargin: -10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,11 +43,12 @@ Flickable {
|
|||||||
Column {
|
Column {
|
||||||
padding: 10
|
padding: 10
|
||||||
id: settingsColumn1
|
id: settingsColumn1
|
||||||
width: settingsPage.width / 2 - padding
|
width: settingsPage.width / 2
|
||||||
|
spacing: 15
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: basicSettingsGroupBox
|
id: basicSettingsGroupBox
|
||||||
width: (parent.width - 2 * parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Basic Settings</font>"
|
title: "<font color=\"skyblue\">Basic Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -78,6 +75,7 @@ Flickable {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
@@ -165,6 +163,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
id: resolutionComboBox
|
||||||
|
maximumWidth: parent.width / 2
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: resolutionListModel
|
id: resolutionListModel
|
||||||
@@ -281,6 +280,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
|
maximumWidth: parent.width / 2
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -401,6 +401,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: vsyncCheck
|
id: vsyncCheck
|
||||||
|
width: parent.width
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: "V-Sync"
|
text: "V-Sync"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -417,6 +418,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: framePacingCheck
|
id: framePacingCheck
|
||||||
|
width: parent.width
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
text: "Frame pacing"
|
text: "Frame pacing"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -436,7 +438,7 @@ Flickable {
|
|||||||
GroupBox {
|
GroupBox {
|
||||||
|
|
||||||
id: audioSettingsGroupBox
|
id: audioSettingsGroupBox
|
||||||
width: (parent.width - 2 * parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Audio Settings</font>"
|
title: "<font color=\"skyblue\">Audio Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -496,7 +498,7 @@ Flickable {
|
|||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: uiSettingsGroupBox
|
id: uiSettingsGroupBox
|
||||||
width: (parent.width - 2 * parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">UI Settings</font>"
|
title: "<font color=\"skyblue\">UI Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -507,6 +509,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: startMaximizedCheck
|
id: startMaximizedCheck
|
||||||
|
width: parent.width
|
||||||
text: "Maximize Moonlight window on startup"
|
text: "Maximize Moonlight window on startup"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
enabled: SystemProperties.hasWindowManager
|
enabled: SystemProperties.hasWindowManager
|
||||||
@@ -518,6 +521,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: connectionWarningsCheck
|
id: connectionWarningsCheck
|
||||||
|
width: parent.width
|
||||||
text: "Show connection quality warnings"
|
text: "Show connection quality warnings"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.connectionWarnings
|
checked: StreamingPreferences.connectionWarnings
|
||||||
@@ -529,6 +533,7 @@ Flickable {
|
|||||||
CheckBox {
|
CheckBox {
|
||||||
visible: SystemProperties.hasDiscordIntegration
|
visible: SystemProperties.hasDiscordIntegration
|
||||||
id: discordPresenceCheck
|
id: discordPresenceCheck
|
||||||
|
width: parent.width
|
||||||
text: "Discord Rich Presence integration"
|
text: "Discord Rich Presence integration"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.richPresence
|
checked: StreamingPreferences.richPresence
|
||||||
@@ -547,13 +552,15 @@ Flickable {
|
|||||||
|
|
||||||
Column {
|
Column {
|
||||||
padding: 10
|
padding: 10
|
||||||
|
rightPadding: 20
|
||||||
anchors.left: settingsColumn1.right
|
anchors.left: settingsColumn1.right
|
||||||
id: settingsColumn2
|
id: settingsColumn2
|
||||||
width: settingsPage.width / 2 - padding
|
width: settingsPage.width / 2
|
||||||
|
spacing: 15
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: gamepadSettingsGroupBox
|
id: gamepadSettingsGroupBox
|
||||||
width: (parent.width - parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Input Settings</font>"
|
title: "<font color=\"skyblue\">Input Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -564,6 +571,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: singleControllerCheck
|
id: singleControllerCheck
|
||||||
|
width: parent.width
|
||||||
text: "Force gamepad #1 always present"
|
text: "Force gamepad #1 always present"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: !StreamingPreferences.multiController
|
checked: !StreamingPreferences.multiController
|
||||||
@@ -581,6 +589,7 @@ Flickable {
|
|||||||
CheckBox {
|
CheckBox {
|
||||||
id: absoluteMouseCheck
|
id: absoluteMouseCheck
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
width: parent.width
|
||||||
text: "Optimize mouse for remote desktop instead of games"
|
text: "Optimize mouse for remote desktop instead of games"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
enabled: SystemProperties.hasWindowManager
|
enabled: SystemProperties.hasWindowManager
|
||||||
@@ -596,9 +605,27 @@ Flickable {
|
|||||||
You can toggle this while streaming using Ctrl+Alt+Shift+M."
|
You can toggle this while streaming using Ctrl+Alt+Shift+M."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: absoluteTouchCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
width: parent.width
|
||||||
|
text: "Use touchscreen as a trackpad"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: !StreamingPreferences.absoluteTouchMode
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.absoluteTouchMode = !checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer."
|
||||||
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: gamepadMouseCheck
|
id: gamepadMouseCheck
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
width: parent.width
|
||||||
text: "Gamepad mouse mode support"
|
text: "Gamepad mouse mode support"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.gamepadMouse
|
checked: StreamingPreferences.gamepadMouse
|
||||||
@@ -611,12 +638,29 @@ Flickable {
|
|||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
|
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: swapMouseButtonsCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
width: parent.width
|
||||||
|
text: "Swap mouse buttons"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.swapMouseButtons
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.swapMouseButtons = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "When checked, swap the left and right mouse buttons"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: hostSettingsGroupBox
|
id: hostSettingsGroupBox
|
||||||
width: (parent.width - parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -627,7 +671,8 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "Optimize game settings"
|
width: parent.width
|
||||||
|
text: "Optimize game settings for streaming"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.gameOptimizations
|
checked: StreamingPreferences.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -637,6 +682,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: audioPcCheck
|
id: audioPcCheck
|
||||||
|
width: parent.width
|
||||||
text: "Play audio on host PC"
|
text: "Play audio on host PC"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.playAudioOnHost
|
checked: StreamingPreferences.playAudioOnHost
|
||||||
@@ -644,12 +690,28 @@ Flickable {
|
|||||||
StreamingPreferences.playAudioOnHost = checked
|
StreamingPreferences.playAudioOnHost = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: quitAppAfter
|
||||||
|
width: parent.width
|
||||||
|
text: "Quit app on host PC after ending stream"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.quitAppAfter
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.quitAppAfter = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: advancedSettingsGroupBox
|
id: advancedSettingsGroupBox
|
||||||
width: (parent.width - parent.padding)
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
@@ -743,10 +805,10 @@ Flickable {
|
|||||||
text: "HEVC (H.265)"
|
text: "HEVC (H.265)"
|
||||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||||
}
|
}
|
||||||
ListElement {
|
/*ListElement {
|
||||||
text: "HEVC HDR (Experimental)"
|
text: "HEVC HDR (Experimental)"
|
||||||
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -756,6 +818,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: unlockUnsupportedFps
|
id: unlockUnsupportedFps
|
||||||
|
width: parent.width
|
||||||
text: "Unlock unsupported FPS options"
|
text: "Unlock unsupported FPS options"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.unsupportedFps
|
checked: StreamingPreferences.unsupportedFps
|
||||||
@@ -774,6 +837,7 @@ Flickable {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: enableMdns
|
id: enableMdns
|
||||||
|
width: parent.width
|
||||||
text: "Automatically find PCs on the local network (Recommended)"
|
text: "Automatically find PCs on the local network (Recommended)"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.enableMdns
|
checked: StreamingPreferences.enableMdns
|
||||||
@@ -797,12 +861,21 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: quitAppAfter
|
id: detectNetworkBlocking
|
||||||
text: "Quit app after quitting session"
|
width: parent.width
|
||||||
|
text: "Automatically detect blocked connections (Recommended)"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.quitAppAfter
|
checked: StreamingPreferences.detectNetworkBlocking
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
StreamingPreferences.quitAppAfter = checked
|
// This is called on init, so only do the work if we've
|
||||||
|
// actually changed the value.
|
||||||
|
if (StreamingPreferences.detectNetworkBlocking != checked) {
|
||||||
|
StreamingPreferences.detectNetworkBlocking = checked
|
||||||
|
|
||||||
|
// We must save the updated preference to ensure
|
||||||
|
// ComputerManager can observe the change internally.
|
||||||
|
StreamingPreferences.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,12 @@ Item {
|
|||||||
window.visible = true
|
window.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionFinished()
|
function sessionFinished(portTestResult)
|
||||||
{
|
{
|
||||||
|
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||||
|
streamSegueErrorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
|
||||||
|
}
|
||||||
|
|
||||||
// Enable GUI gamepad usage now
|
// Enable GUI gamepad usage now
|
||||||
SdlGamepadKeyNavigation.enable()
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
|
|||||||
+133
-32
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
|
|||||||
this, &AppModel::handleBoxArtLoaded);
|
this, &AppModel::handleBoxArtLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
|
||||||
{
|
{
|
||||||
m_ComputerManager = computerManager;
|
m_ComputerManager = computerManager;
|
||||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
@@ -15,29 +15,23 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
|||||||
|
|
||||||
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
||||||
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
||||||
m_Apps = m_Computer->appList;
|
|
||||||
m_CurrentGameId = m_Computer->currentGameId;
|
m_CurrentGameId = m_Computer->currentGameId;
|
||||||
|
m_ShowHiddenGames = showHiddenGames;
|
||||||
|
|
||||||
|
updateAppList(m_Computer->appList);
|
||||||
}
|
}
|
||||||
|
|
||||||
int AppModel::getRunningAppIndex()
|
int AppModel::getRunningAppId()
|
||||||
{
|
{
|
||||||
if (m_CurrentGameId != 0) {
|
return m_CurrentGameId;
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AppModel::getRunningAppName()
|
QString AppModel::getRunningAppName()
|
||||||
{
|
{
|
||||||
if (m_CurrentGameId != 0) {
|
if (m_CurrentGameId != 0) {
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_AllApps.count(); i++) {
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
if (m_AllApps[i].id == m_CurrentGameId) {
|
||||||
return m_Apps[i].name;
|
return m_AllApps[i].name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,8 +41,8 @@ QString AppModel::getRunningAppName()
|
|||||||
|
|
||||||
Session* AppModel::createSessionForApp(int appIndex)
|
Session* AppModel::createSessionForApp(int appIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(appIndex < m_Apps.count());
|
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||||
NvApp app = m_Apps.at(appIndex);
|
NvApp app = m_VisibleApps.at(appIndex);
|
||||||
|
|
||||||
return new Session(m_Computer, app);
|
return new Session(m_Computer, app);
|
||||||
}
|
}
|
||||||
@@ -60,7 +54,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
|
|||||||
if (parent.isValid())
|
if (parent.isValid())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return m_Apps.count();
|
return m_VisibleApps.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant AppModel::data(const QModelIndex &index, int role) const
|
QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||||
@@ -68,8 +62,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
|||||||
if (!index.isValid())
|
if (!index.isValid())
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
||||||
Q_ASSERT(index.row() < m_Apps.count());
|
Q_ASSERT(index.row() < m_VisibleApps.count());
|
||||||
NvApp app = m_Apps.at(index.row());
|
NvApp app = m_VisibleApps.at(index.row());
|
||||||
|
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
@@ -80,6 +74,10 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
|||||||
case BoxArtRole:
|
case BoxArtRole:
|
||||||
// FIXME: const-correctness
|
// FIXME: const-correctness
|
||||||
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
||||||
|
case HiddenRole:
|
||||||
|
return app.hidden;
|
||||||
|
case AppIdRole:
|
||||||
|
return app.id;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -92,6 +90,8 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
|||||||
names[NameRole] = "name";
|
names[NameRole] = "name";
|
||||||
names[RunningRole] = "running";
|
names[RunningRole] = "running";
|
||||||
names[BoxArtRole] = "boxart";
|
names[BoxArtRole] = "boxart";
|
||||||
|
names[HiddenRole] = "hidden";
|
||||||
|
names[AppIdRole] = "appid";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,112 @@ void AppModel::quitRunningApp()
|
|||||||
m_ComputerManager->quitRunningApp(m_Computer);
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
|
||||||
|
{
|
||||||
|
for (const NvApp& visibleApp : m_VisibleApps) {
|
||||||
|
if (app.id == visibleApp.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
|
||||||
|
{
|
||||||
|
QVector<NvApp> visibleApps;
|
||||||
|
|
||||||
|
for (const NvApp& app : appList) {
|
||||||
|
// Don't immediately hide games that were previously visible. This
|
||||||
|
// allows users to easily uncheck the "Hide App" checkbox if they
|
||||||
|
// check it by mistake.
|
||||||
|
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
|
||||||
|
visibleApps.append(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleApps;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppModel::updateAppList(QVector<NvApp> newList)
|
||||||
|
{
|
||||||
|
m_AllApps = newList;
|
||||||
|
|
||||||
|
QVector<NvApp> newVisibleList = getVisibleApps(newList);
|
||||||
|
|
||||||
|
// Process removals and updates first
|
||||||
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
|
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const NvApp& newApp : newVisibleList) {
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
// If the data changed, update it in our list
|
||||||
|
if (existingApp != newApp) {
|
||||||
|
m_VisibleApps.replace(i, newApp);
|
||||||
|
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
beginRemoveRows(QModelIndex(), i, i);
|
||||||
|
m_VisibleApps.removeAt(i);
|
||||||
|
endRemoveRows();
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process additions now
|
||||||
|
for (const NvApp& newApp : newVisibleList) {
|
||||||
|
int insertionIndex = m_VisibleApps.size();
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
|
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||||
|
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (existingApp.name.toLower() > newApp.name.toLower()) {
|
||||||
|
insertionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
|
||||||
|
m_VisibleApps.insert(insertionIndex, newApp);
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(newVisibleList == m_VisibleApps);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppModel::setAppHidden(int appIndex, bool hidden)
|
||||||
|
{
|
||||||
|
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||||
|
int appId = m_VisibleApps.at(appIndex).id;
|
||||||
|
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
|
||||||
|
for (NvApp& app : m_Computer->appList) {
|
||||||
|
if (app.id == appId) {
|
||||||
|
app.hidden = hidden;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Ignore updates for computers that aren't ours
|
// Ignore updates for computers that aren't ours
|
||||||
@@ -119,20 +225,15 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
// First, process additions/removals from the app list. This
|
// First, process additions/removals from the app list. This
|
||||||
// is required because the new game may now be running, so
|
// is required because the new game may now be running, so
|
||||||
// we can't check that first.
|
// we can't check that first.
|
||||||
if (computer->appList != m_Apps) {
|
if (computer->appList != m_AllApps) {
|
||||||
// Just reset the whole thing if the list changes
|
updateAppList(computer->appList);
|
||||||
beginResetModel();
|
|
||||||
m_Apps = computer->appList;
|
|
||||||
m_CurrentGameId = computer->currentGameId;
|
|
||||||
endResetModel();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, process changes to the active app
|
// Finally, process changes to the active app
|
||||||
if (computer->currentGameId != m_CurrentGameId) {
|
if (computer->currentGameId != m_CurrentGameId) {
|
||||||
// First, invalidate the running state of newly running game
|
// First, invalidate the running state of newly running game
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
if (m_Apps[i].id == computer->currentGameId) {
|
if (m_VisibleApps[i].id == computer->currentGameId) {
|
||||||
emit dataChanged(createIndex(i, 0),
|
emit dataChanged(createIndex(i, 0),
|
||||||
createIndex(i, 0),
|
createIndex(i, 0),
|
||||||
QVector<int>() << RunningRole);
|
QVector<int>() << RunningRole);
|
||||||
@@ -142,8 +243,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
|
|
||||||
// Next, invalidate the running state of the old game (if it exists)
|
// Next, invalidate the running state of the old game (if it exists)
|
||||||
if (m_CurrentGameId != 0) {
|
if (m_CurrentGameId != 0) {
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
if (m_VisibleApps[i].id == m_CurrentGameId) {
|
||||||
emit dataChanged(createIndex(i, 0),
|
emit dataChanged(createIndex(i, 0),
|
||||||
createIndex(i, 0),
|
createIndex(i, 0),
|
||||||
QVector<int>() << RunningRole);
|
QVector<int>() << RunningRole);
|
||||||
@@ -161,7 +262,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
|
|||||||
{
|
{
|
||||||
Q_ASSERT(computer == m_Computer);
|
Q_ASSERT(computer == m_Computer);
|
||||||
|
|
||||||
int index = m_Apps.indexOf(app);
|
int index = m_VisibleApps.indexOf(app);
|
||||||
|
|
||||||
// Make sure we're not delivering a callback to an app that's already been removed
|
// Make sure we're not delivering a callback to an app that's already been removed
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
|||||||
+15
-4
@@ -14,23 +14,27 @@ class AppModel : public QAbstractListModel
|
|||||||
{
|
{
|
||||||
NameRole = Qt::UserRole,
|
NameRole = Qt::UserRole,
|
||||||
RunningRole,
|
RunningRole,
|
||||||
BoxArtRole
|
BoxArtRole,
|
||||||
|
HiddenRole,
|
||||||
|
AppIdRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AppModel(QObject *parent = nullptr);
|
explicit AppModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
// Must be called before any QAbstractListModel functions
|
// Must be called before any QAbstractListModel functions
|
||||||
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
|
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||||
|
|
||||||
Q_INVOKABLE int getRunningAppIndex();
|
Q_INVOKABLE int getRunningAppId();
|
||||||
|
|
||||||
Q_INVOKABLE QString getRunningAppName();
|
Q_INVOKABLE QString getRunningAppName();
|
||||||
|
|
||||||
Q_INVOKABLE void quitRunningApp();
|
Q_INVOKABLE void quitRunningApp();
|
||||||
|
|
||||||
|
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
@@ -46,9 +50,16 @@ signals:
|
|||||||
void computerLost();
|
void computerLost();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateAppList(QVector<NvApp> newList);
|
||||||
|
|
||||||
|
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
|
||||||
|
|
||||||
|
bool isAppCurrentlyVisible(const NvApp& app);
|
||||||
|
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
BoxArtManager m_BoxArtManager;
|
BoxArtManager m_BoxArtManager;
|
||||||
ComputerManager* m_ComputerManager;
|
ComputerManager* m_ComputerManager;
|
||||||
QVector<NvApp> m_Apps;
|
QVector<NvApp> m_VisibleApps, m_AllApps;
|
||||||
int m_CurrentGameId;
|
int m_CurrentGameId;
|
||||||
|
bool m_ShowHiddenGames;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
|
||||||
ComputerModel::ComputerModel(QObject* object)
|
ComputerModel::ComputerModel(QObject* object)
|
||||||
: QAbstractListModel(object) {}
|
: QAbstractListModel(object) {}
|
||||||
|
|
||||||
@@ -128,6 +130,69 @@ void ComputerModel::wakeComputer(int computerIndex)
|
|||||||
QThreadPool::globalInstance()->start(wakeTask);
|
QThreadPool::globalInstance()->start(wakeTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerModel::renameComputer(int computerIndex, QString name)
|
||||||
|
{
|
||||||
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
|
|
||||||
|
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||||
|
QString ComputerModel::generatePinString()
|
||||||
|
{
|
||||||
|
std::uniform_int_distribution<int> dist(0, 9999);
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 engine(rd());
|
||||||
|
|
||||||
|
return QString::asprintf("%04u", dist(engine));
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeferredTestConnectionTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
|
||||||
|
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
|
||||||
|
emit connectionTestCompleted(-1, QString());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QString blockedPorts;
|
||||||
|
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
if (portTestResult & (1 << i)) {
|
||||||
|
if (!blockedPorts.isEmpty()) {
|
||||||
|
blockedPorts += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LiGetProtocolFromPortFlagIndex(i) == 17 /* IPPROTO_UDP */) {
|
||||||
|
blockedPorts += "UDP ";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
blockedPorts += "TCP ";
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedPorts += QString::number(LiGetPortFromPortFlagIndex(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit connectionTestCompleted(portTestResult, blockedPorts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void connectionTestCompleted(int result, QString blockedPorts);
|
||||||
|
};
|
||||||
|
|
||||||
|
void ComputerModel::testConnectionForComputer(int)
|
||||||
|
{
|
||||||
|
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
|
||||||
|
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
|
||||||
|
this, &ComputerModel::connectionTestCompleted);
|
||||||
|
QThreadPool::globalInstance()->start(testConnectionTask);
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||||
{
|
{
|
||||||
Q_ASSERT(computerIndex < m_Computers.count());
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
@@ -158,3 +223,4 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "computermodel.moc"
|
||||||
|
|||||||
@@ -31,14 +31,21 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void deleteComputer(int computerIndex);
|
Q_INVOKABLE void deleteComputer(int computerIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString generatePinString();
|
||||||
|
|
||||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||||
|
|
||||||
|
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
|
||||||
|
|
||||||
Q_INVOKABLE void wakeComputer(int computerIndex);
|
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void pairingCompleted(QVariant error);
|
void pairingCompleted(QVariant error);
|
||||||
|
void connectionTestCompleted(int result, QString blockedPorts);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|||||||
+49
-3
@@ -175,6 +175,17 @@ ApplicationWindow {
|
|||||||
anchors.topMargin: 5
|
anchors.topMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: titleLabel
|
||||||
|
visible: toolBar.width > 700
|
||||||
|
anchors.fill: parent
|
||||||
|
text: stackView.currentItem.objectName
|
||||||
|
font.pointSize: 20
|
||||||
|
elide: Label.ElideRight
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
verticalAlignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: 10
|
||||||
@@ -194,14 +205,49 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This label will appear when the window gets too small and
|
||||||
|
// we need to ensure the toolbar controls don't collide
|
||||||
Label {
|
Label {
|
||||||
id: titleLabel
|
id: titleRowLabel
|
||||||
text: stackView.currentItem.objectName
|
font.pointSize: titleLabel.font.pointSize
|
||||||
font.pointSize: 15
|
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
verticalAlignment: Qt.AlignVCenter
|
verticalAlignment: Qt.AlignVCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
// We need this label to always be visible so it can occupy
|
||||||
|
// the remaining space in the RowLayout. To "hide" it, we
|
||||||
|
// just set the text to empty string.
|
||||||
|
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: versionLabel
|
||||||
|
visible: stackView.currentItem.objectName === "Settings"
|
||||||
|
text: "Version " + SystemProperties.versionString
|
||||||
|
font.pointSize: 12
|
||||||
|
horizontalAlignment: Qt.AlignRight
|
||||||
|
verticalAlignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableToolButton {
|
||||||
|
id: discordButton
|
||||||
|
visible: SystemProperties.hasBrowser &&
|
||||||
|
stackView.currentItem.objectName === "Settings"
|
||||||
|
|
||||||
|
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Join our community on Discord"
|
||||||
|
|
||||||
|
// TODO need to make sure browser is brought to foreground.
|
||||||
|
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
|
|||||||
@@ -205,6 +205,31 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In UI navigation mode (settings page), use the right stick to scroll
|
||||||
|
if (m_UiNavMode) {
|
||||||
|
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||||
|
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||||
|
|
||||||
|
QPoint wheelDelta;
|
||||||
|
if (rightX > 30000) {
|
||||||
|
wheelDelta.setX(30);
|
||||||
|
}
|
||||||
|
else if (rightX < -30000) {
|
||||||
|
wheelDelta.setX(-30);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightY > 30000) {
|
||||||
|
wheelDelta.setY(-30);
|
||||||
|
}
|
||||||
|
else if (rightY < -30000) {
|
||||||
|
wheelDelta.setY(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wheelDelta.isNull()) {
|
||||||
|
sendWheel(wheelDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +243,22 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
||||||
|
{
|
||||||
|
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||||
|
QWindow* focusWindow = app->focusWindow();
|
||||||
|
if (focusWindow != nullptr) {
|
||||||
|
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
||||||
|
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||||
|
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
||||||
|
#else
|
||||||
|
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
||||||
|
#endif
|
||||||
|
app->sendEvent(focusWindow, &wheelEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||||
{
|
{
|
||||||
m_UiNavMode = uiNavMode;
|
m_UiNavMode = uiNavMode;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||||
|
void sendWheel(QPoint& angleDelta);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onPollingTimerFired();
|
void onPollingTimerFired();
|
||||||
|
|||||||
+1
-1
@@ -424,7 +424,7 @@ int main(int argc, char *argv[])
|
|||||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||||
}
|
}
|
||||||
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11.");
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||||
qputenv("SDL_VIDEODRIVER", "wayland");
|
qputenv("SDL_VIDEODRIVER", "wayland");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#FFFFFF"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 339 B |
@@ -15,9 +15,13 @@
|
|||||||
<file>res/update.svg</file>
|
<file>res/update.svg</file>
|
||||||
<file>res/baseline-help_outline-24px.svg</file>
|
<file>res/baseline-help_outline-24px.svg</file>
|
||||||
<file>res/baseline-error_outline-24px.svg</file>
|
<file>res/baseline-error_outline-24px.svg</file>
|
||||||
|
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||||
|
<file>res/Discord-Logo-White.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/data">
|
<qresource prefix="/data">
|
||||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||||
|
<file alias="egl.frag">shaders/egl.frag</file>
|
||||||
|
<file alias="egl.vert">shaders/egl.vert</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#define SER_MDNS "mdns"
|
#define SER_MDNS "mdns"
|
||||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||||
#define SER_ABSMOUSEMODE "mouseacceleration"
|
#define SER_ABSMOUSEMODE "mouseacceleration"
|
||||||
|
#define SER_ABSTOUCHMODE "abstouchmode"
|
||||||
#define SER_STARTWINDOWED "startwindowed"
|
#define SER_STARTWINDOWED "startwindowed"
|
||||||
#define SER_FRAMEPACING "framepacing"
|
#define SER_FRAMEPACING "framepacing"
|
||||||
#define SER_CONNWARNINGS "connwarnings"
|
#define SER_CONNWARNINGS "connwarnings"
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
#define SER_GAMEPADMOUSE "gamepadmouse"
|
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||||
#define SER_DEFAULTVER "defaultver"
|
#define SER_DEFAULTVER "defaultver"
|
||||||
#define SER_PACKETSIZE "packetsize"
|
#define SER_PACKETSIZE "packetsize"
|
||||||
|
#define SER_DETECTNETBLOCKING "detectnetblocking"
|
||||||
|
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
|
||||||
|
|
||||||
#define CURRENT_DEFAULT_VER 1
|
#define CURRENT_DEFAULT_VER 1
|
||||||
|
|
||||||
@@ -60,12 +63,15 @@ void StreamingPreferences::reload()
|
|||||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||||
|
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
|
||||||
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
|
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
|
||||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||||
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||||
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||||
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
||||||
|
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
|
||||||
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
|
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
|
||||||
|
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
@@ -104,17 +110,20 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_MDNS, enableMdns);
|
settings.setValue(SER_MDNS, enableMdns);
|
||||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||||
|
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
|
||||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||||
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||||
settings.setValue(SER_RICHPRESENCE, richPresence);
|
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||||
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
||||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||||
|
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||||
|
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||||
}
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
|
|||||||
@@ -62,16 +62,19 @@ public:
|
|||||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||||
|
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
|
||||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||||
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||||
|
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||||
|
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
@@ -86,11 +89,14 @@ public:
|
|||||||
bool enableMdns;
|
bool enableMdns;
|
||||||
bool quitAppAfter;
|
bool quitAppAfter;
|
||||||
bool absoluteMouseMode;
|
bool absoluteMouseMode;
|
||||||
|
bool absoluteTouchMode;
|
||||||
bool startWindowed;
|
bool startWindowed;
|
||||||
bool framePacing;
|
bool framePacing;
|
||||||
bool connectionWarnings;
|
bool connectionWarnings;
|
||||||
bool richPresence;
|
bool richPresence;
|
||||||
bool gamepadMouse;
|
bool gamepadMouse;
|
||||||
|
bool detectNetworkBlocking;
|
||||||
|
bool swapMouseButtons;
|
||||||
int packetSize;
|
int packetSize;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
@@ -109,6 +115,7 @@ signals:
|
|||||||
void enableMdnsChanged();
|
void enableMdnsChanged();
|
||||||
void quitAppAfterChanged();
|
void quitAppAfterChanged();
|
||||||
void absoluteMouseModeChanged();
|
void absoluteMouseModeChanged();
|
||||||
|
void absoluteTouchModeChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
@@ -118,5 +125,7 @@ signals:
|
|||||||
void connectionWarningsChanged();
|
void connectionWarningsChanged();
|
||||||
void richPresenceChanged();
|
void richPresenceChanged();
|
||||||
void gamepadMouseChanged();
|
void gamepadMouseChanged();
|
||||||
|
void detectNetworkBlockingChanged();
|
||||||
|
void mouseButtonsChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#version 300 es
|
||||||
|
#extension GL_OES_EGL_image_external : require
|
||||||
|
precision mediump float;
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
in vec2 vTextCoord;
|
||||||
|
|
||||||
|
uniform mat3 yuvmat;
|
||||||
|
uniform vec3 offset;
|
||||||
|
uniform samplerExternalOES plane1;
|
||||||
|
uniform samplerExternalOES plane2;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec3 YCbCr = vec3(
|
||||||
|
texture2D(plane1, vTextCoord)[0],
|
||||||
|
texture2D(plane2, vTextCoord).xy
|
||||||
|
);
|
||||||
|
|
||||||
|
YCbCr -= offset;
|
||||||
|
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#version 300 es
|
||||||
|
|
||||||
|
layout (location = 0) in vec2 aPosition; // 2D: X,Y
|
||||||
|
layout (location = 1) in vec2 aTexCoord;
|
||||||
|
out vec2 vTextCoord;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vTextCoord = aTexCoord;
|
||||||
|
gl_Position = vec4(aPosition, 0, 1);
|
||||||
|
}
|
||||||
@@ -1,1483 +0,0 @@
|
|||||||
#include <Limelight.h>
|
|
||||||
#include <SDL.h>
|
|
||||||
#include "streaming/session.h"
|
|
||||||
#include "settings/mappingmanager.h"
|
|
||||||
#include "path.h"
|
|
||||||
#include "streamutils.h"
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
#include <QtMath>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QGuiApplication>
|
|
||||||
|
|
||||||
#define VK_0 0x30
|
|
||||||
#define VK_A 0x41
|
|
||||||
|
|
||||||
// These are real Windows VK_* codes
|
|
||||||
#ifndef VK_F1
|
|
||||||
#define VK_F1 0x70
|
|
||||||
#define VK_F13 0x7C
|
|
||||||
#define VK_NUMPAD0 0x60
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define MOUSE_POLLING_INTERVAL 5
|
|
||||||
|
|
||||||
// How long the fingers must be stationary to start a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELAY 650
|
|
||||||
|
|
||||||
// How far the finger can move before it cancels a right click
|
|
||||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
|
||||||
|
|
||||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
|
|
||||||
|
|
||||||
// How far the finger can move before it can override the double tap deadzone
|
|
||||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
|
||||||
|
|
||||||
// How long the Start button must be pressed to toggle mouse emulation
|
|
||||||
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
|
|
||||||
|
|
||||||
// How long between polling the gamepad to send virtual mouse input
|
|
||||||
#define MOUSE_EMULATION_POLLING_INTERVAL 50
|
|
||||||
|
|
||||||
// Determines how fast the mouse will move each interval
|
|
||||||
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
|
|
||||||
|
|
||||||
// Determines the maximum motion amount before allowing movement
|
|
||||||
#define MOUSE_EMULATION_DEADZONE 2
|
|
||||||
|
|
||||||
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
|
||||||
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
|
||||||
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
|
||||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
|
||||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
|
||||||
LS_CLK_FLAG, RS_CLK_FLAG,
|
|
||||||
LB_FLAG, RB_FLAG,
|
|
||||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
|
||||||
};
|
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
|
||||||
: m_MultiController(prefs.multiController),
|
|
||||||
m_GamepadMouse(prefs.gamepadMouse),
|
|
||||||
m_MouseMoveTimer(0),
|
|
||||||
m_FakeCaptureActive(false),
|
|
||||||
m_LongPressTimer(0),
|
|
||||||
m_StreamWidth(streamWidth),
|
|
||||||
m_StreamHeight(streamHeight),
|
|
||||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode)
|
|
||||||
{
|
|
||||||
// Allow gamepad input when the app doesn't have focus
|
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
|
||||||
|
|
||||||
// If absolute mouse mode is enabled, use relative mode warp (which
|
|
||||||
// is via normal motion events that are influenced by mouse acceleration).
|
|
||||||
// Otherwise, we'll use raw input capture which is straight from the device
|
|
||||||
// without modification by the OS.
|
|
||||||
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
|
||||||
prefs.absoluteMouseMode ? "1" : "0",
|
|
||||||
SDL_HINT_OVERRIDE);
|
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
|
|
||||||
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
|
|
||||||
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
|
|
||||||
// SDL 2.0.10 where the bug is fixed.
|
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/issues/133
|
|
||||||
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
|
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
// Enabling extended input reports allows rumble to function on Bluetooth PS4
|
|
||||||
// controllers, but breaks DirectInput applications. We will enable it because
|
|
||||||
// it's likely that working rumble is what the user is expecting. If they don't
|
|
||||||
// want this behavior, they can override it with the environment variable.
|
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// We must initialize joystick explicitly before gamecontroller in order
|
|
||||||
// to ensure we receive gamecontroller attach events for gamepads where
|
|
||||||
// SDL doesn't have a built-in mapping. By starting joystick first, we
|
|
||||||
// can allow mapping manager to update the mappings before GC attach
|
|
||||||
// events are generated.
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
MappingManager mappingManager;
|
|
||||||
mappingManager.applyMappings();
|
|
||||||
|
|
||||||
// Flush gamepad arrival and departure events which may be queued before
|
|
||||||
// starting the gamecontroller subsystem again. This prevents us from
|
|
||||||
// receiving duplicate arrival and departure events for the same gamepad.
|
|
||||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
|
||||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
|
|
||||||
|
|
||||||
// We need to reinit this each time, since you only get
|
|
||||||
// an initial set of gamepad arrival events once per init.
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
|
||||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
|
||||||
// during stream startup as we detect currently attached gamepads one at a time.
|
|
||||||
m_GamepadMask = getAttachedGamepadMask();
|
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
|
||||||
SDL_zero(m_LastTouchDownEvent);
|
|
||||||
SDL_zero(m_LastTouchUpEvent);
|
|
||||||
|
|
||||||
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
|
||||||
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
|
||||||
|
|
||||||
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
|
|
||||||
if (pollingInterval == 0) {
|
|
||||||
pollingInterval = MOUSE_POLLING_INTERVAL;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Using custom mouse polling interval: %u ms",
|
|
||||||
pollingInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
|
||||||
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
|
||||||
Session::get()->notifyMouseEmulationMode(false);
|
|
||||||
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
|
|
||||||
}
|
|
||||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
if (m_GamepadState[i].haptic != nullptr) {
|
|
||||||
SDL_HapticClose(m_GamepadState[i].haptic);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (m_GamepadState[i].controller != nullptr) {
|
|
||||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RemoveTimer(m_MouseMoveTimer);
|
|
||||||
SDL_RemoveTimer(m_LongPressTimer);
|
|
||||||
|
|
||||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
|
||||||
|
|
||||||
// Return background event handling to off
|
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
|
|
||||||
|
|
||||||
#ifdef STEAM_LINK
|
|
||||||
// Hide SDL's cursor on Steam Link after quitting the stream.
|
|
||||||
// FIXME: We should also do this for other situations where SDL
|
|
||||||
// and Qt will draw their own mouse cursors like KMSDRM or RPi
|
|
||||||
// video backends.
|
|
||||||
SDL_ShowCursor(SDL_DISABLE);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|
||||||
{
|
|
||||||
short keyCode;
|
|
||||||
char modifiers;
|
|
||||||
|
|
||||||
// Check for our special key combos
|
|
||||||
if ((event->state == SDL_PRESSED) &&
|
|
||||||
(event->keysym.mod & KMOD_CTRL) &&
|
|
||||||
(event->keysym.mod & KMOD_ALT) &&
|
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
|
||||||
// First we test the SDLK combos for matches,
|
|
||||||
// that way we ensure that latin keyboard users
|
|
||||||
// can match to the key they see on their keyboards.
|
|
||||||
// If nothing matches that, we'll then go on to
|
|
||||||
// checking scancodes so non-latin keyboard users
|
|
||||||
// can have working hotkeys (though possibly in
|
|
||||||
// odd positions). We must do all SDLK tests before
|
|
||||||
// any scancode tests to avoid issues in cases
|
|
||||||
// where the SDLK for one shortcut collides with
|
|
||||||
// the scancode of another.
|
|
||||||
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
|
||||||
if (event->keysym.sym == SDLK_q) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected quit key combo (SDLK)");
|
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
|
||||||
SDL_Event event;
|
|
||||||
event.type = SDL_QUIT;
|
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
|
||||||
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected mouse capture toggle combo (SDLK)");
|
|
||||||
|
|
||||||
// Stop handling future input
|
|
||||||
setCaptureActive(!isCaptureActive());
|
|
||||||
|
|
||||||
// Force raise all keys to ensure they aren't stuck,
|
|
||||||
// since we won't get their key up events.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
|
||||||
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected mouse mode toggle combo (SDLK)");
|
|
||||||
|
|
||||||
// Uncapture input
|
|
||||||
setCaptureActive(false);
|
|
||||||
|
|
||||||
// Toggle mouse mode
|
|
||||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
|
||||||
|
|
||||||
// Recapture input
|
|
||||||
setCaptureActive(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected full-screen toggle combo (SDLK)");
|
|
||||||
Session::s_ActiveSession->toggleFullscreen();
|
|
||||||
|
|
||||||
// Force raise all keys just be safe across this full-screen/windowed
|
|
||||||
// transition just in case key events get lost.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
|
||||||
else if (event->keysym.sym == SDLK_s) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected stats toggle combo (SDLK)");
|
|
||||||
|
|
||||||
// Toggle the stats overlay
|
|
||||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
|
||||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
|
||||||
|
|
||||||
// Force raise all keys just be safe across this full-screen/windowed
|
|
||||||
// transition just in case key events get lost.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
|
||||||
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected quit key combo (scancode)");
|
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
|
||||||
SDL_Event event;
|
|
||||||
event.type = SDL_QUIT;
|
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
|
||||||
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected mouse capture toggle combo (scancode)");
|
|
||||||
|
|
||||||
// Stop handling future input
|
|
||||||
setCaptureActive(!isCaptureActive());
|
|
||||||
|
|
||||||
// Force raise all keys to ensure they aren't stuck,
|
|
||||||
// since we won't get their key up events.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
|
||||||
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected full-screen toggle combo (scancode)");
|
|
||||||
Session::s_ActiveSession->toggleFullscreen();
|
|
||||||
|
|
||||||
// Force raise all keys just be safe across this full-screen/windowed
|
|
||||||
// transition just in case key events get lost.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
|
||||||
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected mouse mode toggle combo (scancode)");
|
|
||||||
|
|
||||||
// Uncapture input
|
|
||||||
setCaptureActive(false);
|
|
||||||
|
|
||||||
// Toggle mouse mode
|
|
||||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
|
||||||
|
|
||||||
// Recapture input
|
|
||||||
setCaptureActive(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected stats toggle combo (scancode)");
|
|
||||||
|
|
||||||
// Toggle the stats overlay
|
|
||||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
|
||||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
|
||||||
|
|
||||||
// Force raise all keys just be safe across this full-screen/windowed
|
|
||||||
// transition just in case key events get lost.
|
|
||||||
raiseAllKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->repeat) {
|
|
||||||
// Ignore repeat key down events
|
|
||||||
SDL_assert(event->state == SDL_PRESSED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set modifier flags
|
|
||||||
modifiers = 0;
|
|
||||||
if (event->keysym.mod & KMOD_CTRL) {
|
|
||||||
modifiers |= MODIFIER_CTRL;
|
|
||||||
}
|
|
||||||
if (event->keysym.mod & KMOD_ALT) {
|
|
||||||
modifiers |= MODIFIER_ALT;
|
|
||||||
}
|
|
||||||
if (event->keysym.mod & KMOD_SHIFT) {
|
|
||||||
modifiers |= MODIFIER_SHIFT;
|
|
||||||
}
|
|
||||||
if (event->keysym.mod & KMOD_GUI) {
|
|
||||||
modifiers |= MODIFIER_META;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
|
||||||
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
|
||||||
// a QWERTY layout to work.
|
|
||||||
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
|
|
||||||
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
|
|
||||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
|
|
||||||
}
|
|
||||||
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
|
|
||||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
|
|
||||||
}
|
|
||||||
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
|
|
||||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
|
|
||||||
}
|
|
||||||
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
|
|
||||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
|
|
||||||
}
|
|
||||||
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
|
|
||||||
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
|
|
||||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
switch (event->keysym.scancode) {
|
|
||||||
case SDL_SCANCODE_BACKSPACE:
|
|
||||||
keyCode = 0x08;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_TAB:
|
|
||||||
keyCode = 0x09;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_CLEAR:
|
|
||||||
keyCode = 0x0C;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
|
|
||||||
case SDL_SCANCODE_RETURN:
|
|
||||||
keyCode = 0x0D;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_PAUSE:
|
|
||||||
keyCode = 0x13;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_CAPSLOCK:
|
|
||||||
keyCode = 0x14;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_ESCAPE:
|
|
||||||
keyCode = 0x1B;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_SPACE:
|
|
||||||
keyCode = 0x20;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_PAGEUP:
|
|
||||||
keyCode = 0x21;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_PAGEDOWN:
|
|
||||||
keyCode = 0x22;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_END:
|
|
||||||
keyCode = 0x23;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_HOME:
|
|
||||||
keyCode = 0x24;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_LEFT:
|
|
||||||
keyCode = 0x25;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_UP:
|
|
||||||
keyCode = 0x26;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RIGHT:
|
|
||||||
keyCode = 0x27;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_DOWN:
|
|
||||||
keyCode = 0x28;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_SELECT:
|
|
||||||
keyCode = 0x29;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_EXECUTE:
|
|
||||||
keyCode = 0x2B;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_PRINTSCREEN:
|
|
||||||
keyCode = 0x2C;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_INSERT:
|
|
||||||
keyCode = 0x2D;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_DELETE:
|
|
||||||
keyCode = 0x2E;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_HELP:
|
|
||||||
keyCode = 0x2F;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_0:
|
|
||||||
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
|
|
||||||
keyCode = VK_NUMPAD0;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_0:
|
|
||||||
// See comment above about why we only handle SDL_SCANCODE_0 here
|
|
||||||
keyCode = VK_0;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_MULTIPLY:
|
|
||||||
keyCode = 0x6A;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_PLUS:
|
|
||||||
keyCode = 0x6B;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_COMMA:
|
|
||||||
keyCode = 0x6C;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_MINUS:
|
|
||||||
keyCode = 0x6D;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_PERIOD:
|
|
||||||
keyCode = 0x6E;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_KP_DIVIDE:
|
|
||||||
keyCode = 0x6F;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
|
||||||
keyCode = 0x90;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_SCROLLLOCK:
|
|
||||||
keyCode = 0x91;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_LSHIFT:
|
|
||||||
keyCode = 0xA0;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RSHIFT:
|
|
||||||
keyCode = 0xA1;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_LCTRL:
|
|
||||||
keyCode = 0xA2;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RCTRL:
|
|
||||||
keyCode = 0xA3;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_LALT:
|
|
||||||
keyCode = 0xA4;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RALT:
|
|
||||||
keyCode = 0xA5;
|
|
||||||
break;
|
|
||||||
// Until we can fully capture these on all platforms (without conflicting with
|
|
||||||
// OS-provided shortcuts), we should avoid passing them through to the host.
|
|
||||||
#if 0
|
|
||||||
case SDL_SCANCODE_LGUI:
|
|
||||||
keyCode = 0x5B;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RGUI:
|
|
||||||
keyCode = 0x5C;
|
|
||||||
break;
|
|
||||||
#endif
|
|
||||||
case SDL_SCANCODE_AC_BACK:
|
|
||||||
keyCode = 0xA6;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_FORWARD:
|
|
||||||
keyCode = 0xA7;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_REFRESH:
|
|
||||||
keyCode = 0xA8;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_STOP:
|
|
||||||
keyCode = 0xA9;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_SEARCH:
|
|
||||||
keyCode = 0xAA;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_BOOKMARKS:
|
|
||||||
keyCode = 0xAB;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_AC_HOME:
|
|
||||||
keyCode = 0xAC;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_SEMICOLON:
|
|
||||||
keyCode = 0xBA;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_EQUALS:
|
|
||||||
keyCode = 0xBB;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_COMMA:
|
|
||||||
keyCode = 0xBC;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_MINUS:
|
|
||||||
keyCode = 0xBD;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_PERIOD:
|
|
||||||
keyCode = 0xBE;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_SLASH:
|
|
||||||
keyCode = 0xBF;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_GRAVE:
|
|
||||||
keyCode = 0xC0;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_LEFTBRACKET:
|
|
||||||
keyCode = 0xDB;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_BACKSLASH:
|
|
||||||
keyCode = 0xDC;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_RIGHTBRACKET:
|
|
||||||
keyCode = 0xDD;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_APOSTROPHE:
|
|
||||||
keyCode = 0xDE;
|
|
||||||
break;
|
|
||||||
case SDL_SCANCODE_NONUSBACKSLASH:
|
|
||||||
keyCode = 0xE2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unhandled button event: %d",
|
|
||||||
event->keysym.scancode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track the key state so we always know which keys are down
|
|
||||||
if (event->state == SDL_PRESSED) {
|
|
||||||
m_KeysDown.insert(keyCode);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_KeysDown.remove(keyCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
LiSendKeyboardEvent(keyCode,
|
|
||||||
event->state == SDL_PRESSED ?
|
|
||||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
|
||||||
modifiers);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|
||||||
{
|
|
||||||
int button;
|
|
||||||
|
|
||||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
|
||||||
// Ignore synthetic mouse events
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (!isCaptureActive()) {
|
|
||||||
if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) {
|
|
||||||
// Capture the mouse again if clicked when unbound.
|
|
||||||
// We start capture on left button released instead of
|
|
||||||
// pressed to avoid sending an errant mouse button released
|
|
||||||
// event to the host when clicking into our window (since
|
|
||||||
// the pressed event was consumed by this code).
|
|
||||||
setCaptureActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not capturing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event->button)
|
|
||||||
{
|
|
||||||
case SDL_BUTTON_LEFT:
|
|
||||||
button = BUTTON_LEFT;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_MIDDLE:
|
|
||||||
button = BUTTON_MIDDLE;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_RIGHT:
|
|
||||||
button = BUTTON_RIGHT;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_X1:
|
|
||||||
button = BUTTON_X1;
|
|
||||||
break;
|
|
||||||
case SDL_BUTTON_X2:
|
|
||||||
button = BUTTON_X2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unhandled button event: %d",
|
|
||||||
event->button);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
|
|
||||||
BUTTON_ACTION_PRESS :
|
|
||||||
BUTTON_ACTION_RELEASE,
|
|
||||||
button);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleMouseMotionEvent(SDL_Window* window, SDL_MouseMotionEvent* event)
|
|
||||||
{
|
|
||||||
if (!isCaptureActive()) {
|
|
||||||
// Not capturing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
|
||||||
// Ignore synthetic mouse events
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_AbsoluteMouseMode) {
|
|
||||||
SDL_Rect src, dst;
|
|
||||||
|
|
||||||
src.x = src.y = 0;
|
|
||||||
src.w = m_StreamWidth;
|
|
||||||
src.h = m_StreamHeight;
|
|
||||||
|
|
||||||
dst.x = dst.y = 0;
|
|
||||||
SDL_GetWindowSize(window, &dst.w, &dst.h);
|
|
||||||
|
|
||||||
// Use the stream and window sizes to determine the video region
|
|
||||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
|
||||||
|
|
||||||
// Clamp motion to the video region
|
|
||||||
short x = qMin(qMax(event->x - dst.x, 0), dst.w);
|
|
||||||
short y = qMin(qMax(event->y - dst.y, 0), dst.h);
|
|
||||||
|
|
||||||
// Send the mouse position update
|
|
||||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Batch until the next mouse polling window or we'll get awful
|
|
||||||
// input lag everything except GFE 3.14 and 3.15.
|
|
||||||
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
|
|
||||||
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
|
||||||
{
|
|
||||||
if (!isCaptureActive()) {
|
|
||||||
// Not capturing
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
|
||||||
// Ignore synthetic mouse events
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->y != 0) {
|
|
||||||
LiSendScrollEvent((signed char)event->y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GamepadState*
|
|
||||||
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; i < MAX_GAMEPADS; i++) {
|
|
||||||
if (m_GamepadState[i].jsId == id) {
|
|
||||||
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
|
|
||||||
return &m_GamepadState[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only happen with > 4 gamepads
|
|
||||||
SDL_assert(SDL_NumJoysticks() > 4);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
|
||||||
{
|
|
||||||
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
|
|
||||||
LiSendMultiControllerEvent(state->index,
|
|
||||||
m_GamepadMask,
|
|
||||||
state->buttons,
|
|
||||||
state->lt,
|
|
||||||
state->rt,
|
|
||||||
state->lsX,
|
|
||||||
state->lsY,
|
|
||||||
state->rsX,
|
|
||||||
state->rsY);
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
|
|
||||||
{
|
|
||||||
// Raise the left click and start a right click
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
|
||||||
{
|
|
||||||
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
|
||||||
|
|
||||||
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
|
|
||||||
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
|
|
||||||
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
|
||||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return interval;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
|
||||||
{
|
|
||||||
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
|
||||||
|
|
||||||
short rawX;
|
|
||||||
short rawY;
|
|
||||||
|
|
||||||
// Determine which analog stick is currently receiving the strongest input
|
|
||||||
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
|
|
||||||
rawX = gamepad->lsX;
|
|
||||||
rawY = -gamepad->lsY;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rawX = gamepad->rsX;
|
|
||||||
rawY = -gamepad->rsY;
|
|
||||||
}
|
|
||||||
|
|
||||||
float deltaX;
|
|
||||||
float deltaY;
|
|
||||||
|
|
||||||
// Produce a base vector for mouse movement with increased speed as we deviate further from center
|
|
||||||
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
|
||||||
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
|
||||||
|
|
||||||
// Enforce deadzones
|
|
||||||
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
|
|
||||||
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
|
|
||||||
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
|
||||||
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return interval;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
|
||||||
{
|
|
||||||
SDL_JoystickID gameControllerId = event->which;
|
|
||||||
GamepadState* state = findStateForGamepad(gameControllerId);
|
|
||||||
if (state == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch all pending axis motion events for this gamepad to save CPU time
|
|
||||||
SDL_Event nextEvent;
|
|
||||||
for (;;) {
|
|
||||||
switch (event->axis)
|
|
||||||
{
|
|
||||||
case SDL_CONTROLLER_AXIS_LEFTX:
|
|
||||||
state->lsX = event->value;
|
|
||||||
break;
|
|
||||||
case SDL_CONTROLLER_AXIS_LEFTY:
|
|
||||||
// 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 = -qMax(event->value, (short)-32767);
|
|
||||||
break;
|
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
|
||||||
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
|
||||||
break;
|
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
|
||||||
state->rt = (unsigned char)(event->value * 255UL / 32767);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unhandled controller axis: %d",
|
|
||||||
event->axis);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for another event to batch with
|
|
||||||
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
event = &nextEvent.caxis;
|
|
||||||
if (event->which != gameControllerId) {
|
|
||||||
// Stop batching if a different gamepad interrupts us
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the next event to batch
|
|
||||||
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
|
||||||
if (state->mouseEmulationTimer == 0) {
|
|
||||||
sendGamepadState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
|
||||||
{
|
|
||||||
GamepadState* state = findStateForGamepad(event->which);
|
|
||||||
if (state == NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->state == SDL_PRESSED) {
|
|
||||||
state->buttons |= k_ButtonMap[event->button];
|
|
||||||
|
|
||||||
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
|
||||||
state->lastStartDownTime = SDL_GetTicks();
|
|
||||||
}
|
|
||||||
else if (state->mouseEmulationTimer != 0) {
|
|
||||||
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
|
|
||||||
LiSendScrollEvent(1);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
|
||||||
LiSendScrollEvent(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state->buttons &= ~k_ButtonMap[event->button];
|
|
||||||
|
|
||||||
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
|
||||||
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
|
|
||||||
if (state->mouseEmulationTimer != 0) {
|
|
||||||
SDL_RemoveTimer(state->mouseEmulationTimer);
|
|
||||||
state->mouseEmulationTimer = 0;
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Mouse emulation deactivated");
|
|
||||||
Session::get()->notifyMouseEmulationMode(false);
|
|
||||||
}
|
|
||||||
else if (m_GamepadMouse) {
|
|
||||||
// Send the start button up event to the host, since we won't do it below
|
|
||||||
sendGamepadState(state);
|
|
||||||
|
|
||||||
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Mouse emulation active");
|
|
||||||
Session::get()->notifyMouseEmulationMode(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (state->mouseEmulationTimer != 0) {
|
|
||||||
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
|
|
||||||
}
|
|
||||||
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
|
||||||
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Detected quit gamepad button combo");
|
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
|
||||||
SDL_Event event;
|
|
||||||
event.type = SDL_QUIT;
|
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
|
|
||||||
// Clear buttons down on this gameapd
|
|
||||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
|
||||||
0, 0, 0, 0, 0, 0, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
|
||||||
if (state->mouseEmulationTimer == 0) {
|
|
||||||
sendGamepadState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
|
||||||
{
|
|
||||||
GamepadState* state;
|
|
||||||
|
|
||||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
|
||||||
int i;
|
|
||||||
const char* name;
|
|
||||||
SDL_GameController* controller;
|
|
||||||
const char* mapping;
|
|
||||||
char guidStr[33];
|
|
||||||
uint32_t hapticCaps;
|
|
||||||
|
|
||||||
controller = SDL_GameControllerOpen(event->which);
|
|
||||||
if (controller == NULL) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open gamepad: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
|
||||||
// can lead to strange issues due to bugs in Windows where an Xbox
|
|
||||||
// controller will join as player 2, even though no player 1 controller
|
|
||||||
// is connected at all. This pretty much screws any attempt to use
|
|
||||||
// the gamepad in single player games, so just assign them in order from 0.
|
|
||||||
i = 0;
|
|
||||||
|
|
||||||
for (; i < MAX_GAMEPADS; i++) {
|
|
||||||
SDL_assert(m_GamepadState[i].controller != controller);
|
|
||||||
if (m_GamepadState[i].controller == NULL) {
|
|
||||||
// Found an empty slot
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == MAX_GAMEPADS) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"No open gamepad slots found!");
|
|
||||||
SDL_GameControllerClose(controller);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = &m_GamepadState[i];
|
|
||||||
if (m_MultiController) {
|
|
||||||
state->index = i;
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 12)
|
|
||||||
// This will change indicators on the controller to show the assigned
|
|
||||||
// player index. For Xbox 360 controllers, that means updating the LED
|
|
||||||
// ring to light up the corresponding quadrant for this player.
|
|
||||||
SDL_GameControllerSetPlayerIndex(controller, state->index);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Always player 1 in single controller mode
|
|
||||||
state->index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
state->controller = controller;
|
|
||||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
// Perform a tiny rumble to see if haptics are supported.
|
|
||||||
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
|
|
||||||
// and we'll get a (potentially false) success value returned.
|
|
||||||
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
|
|
||||||
ML_HAPTIC_GC_RUMBLE : 0;
|
|
||||||
#else
|
|
||||||
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
|
||||||
state->hapticEffectId = -1;
|
|
||||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
|
|
||||||
if (state->haptic != nullptr) {
|
|
||||||
// Query for supported haptic effects
|
|
||||||
hapticCaps = SDL_HapticQuery(state->haptic);
|
|
||||||
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
|
|
||||||
ML_HAPTIC_SIMPLE_RUMBLE : 0;
|
|
||||||
|
|
||||||
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
|
|
||||||
if (SDL_HapticRumbleSupported(state->haptic)) {
|
|
||||||
if (SDL_HapticRumbleInit(state->haptic) == 0) {
|
|
||||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
|
|
||||||
SDL_HapticClose(state->haptic);
|
|
||||||
state->haptic = nullptr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
hapticCaps = 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
|
||||||
guidStr, sizeof(guidStr));
|
|
||||||
mapping = SDL_GameControllerMapping(state->controller);
|
|
||||||
name = SDL_GameControllerName(state->controller);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
|
||||||
i,
|
|
||||||
state->index,
|
|
||||||
name != nullptr ? name : "<null>",
|
|
||||||
hapticCaps,
|
|
||||||
guidStr,
|
|
||||||
mapping != nullptr ? mapping : "<null>");
|
|
||||||
if (mapping != nullptr) {
|
|
||||||
SDL_free((void*)mapping);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this gamepad to the gamepad mask
|
|
||||||
if (m_MultiController) {
|
|
||||||
// NB: Don't assert that it's unset here because we will already
|
|
||||||
// have the mask set for initially attached gamepads to avoid confusing
|
|
||||||
// apps running on the host.
|
|
||||||
m_GamepadMask |= (1 << state->index);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_assert(m_GamepadMask == 0x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send an empty event to tell the PC we've arrived
|
|
||||||
sendGamepadState(state);
|
|
||||||
}
|
|
||||||
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
|
||||||
state = findStateForGamepad(event->which);
|
|
||||||
if (state != NULL) {
|
|
||||||
if (state->mouseEmulationTimer != 0) {
|
|
||||||
Session::get()->notifyMouseEmulationMode(false);
|
|
||||||
SDL_RemoveTimer(state->mouseEmulationTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_GameControllerClose(state->controller);
|
|
||||||
|
|
||||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
if (state->haptic != nullptr) {
|
|
||||||
SDL_HapticClose(state->haptic);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Remove this from the gamepad mask in MC-mode
|
|
||||||
if (m_MultiController) {
|
|
||||||
SDL_assert(m_GamepadMask & (1 << state->index));
|
|
||||||
m_GamepadMask &= ~(1 << state->index);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_assert(m_GamepadMask == 0x1);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Gamepad %d is gone",
|
|
||||||
state->index);
|
|
||||||
|
|
||||||
// Send a final event to let the PC know this gamepad is gone
|
|
||||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
|
||||||
0, 0, 0, 0, 0, 0, 0);
|
|
||||||
|
|
||||||
// Clear all remaining state from this slot
|
|
||||||
SDL_memset(state, 0, sizeof(*state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
|
||||||
{
|
|
||||||
SDL_assert(event->type == SDL_JOYDEVICEADDED);
|
|
||||||
|
|
||||||
if (!SDL_IsGameController(event->which)) {
|
|
||||||
char guidStr[33];
|
|
||||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
|
|
||||||
guidStr, sizeof(guidStr));
|
|
||||||
const char* name = SDL_JoystickNameForIndex(event->which);
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Joystick discovered with no mapping: %s %s",
|
|
||||||
name ? name : "<UNKNOWN>",
|
|
||||||
guidStr);
|
|
||||||
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
|
|
||||||
if (joy != nullptr) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
|
||||||
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
|
|
||||||
SDL_JoystickNumHats(joy));
|
|
||||||
SDL_JoystickClose(joy);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to open joystick for query: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
|
||||||
{
|
|
||||||
// Make sure the controller number is within our supported count
|
|
||||||
if (controllerNumber >= MAX_GAMEPADS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
||||||
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
|
||||||
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Check if the controller supports haptics (and if the controller exists at all)
|
|
||||||
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
|
|
||||||
if (haptic == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the last effect we played
|
|
||||||
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
|
||||||
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
|
||||||
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
|
|
||||||
}
|
|
||||||
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
|
||||||
SDL_HapticRumbleStop(haptic);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this callback is telling us to stop both motors, don't bother queuing a new effect
|
|
||||||
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
|
||||||
SDL_HapticEffect effect;
|
|
||||||
SDL_memset(&effect, 0, sizeof(effect));
|
|
||||||
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
|
||||||
|
|
||||||
// The effect should last until we are instructed to stop or change it
|
|
||||||
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
|
||||||
|
|
||||||
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
|
|
||||||
effect.leftright.large_magnitude = lowFreqMotor / 2;
|
|
||||||
effect.leftright.small_magnitude = highFreqMotor / 2;
|
|
||||||
|
|
||||||
// Play the new effect
|
|
||||||
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
|
||||||
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
|
||||||
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
|
|
||||||
}
|
|
||||||
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
|
||||||
SDL_HapticRumblePlay(haptic,
|
|
||||||
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
|
|
||||||
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
|
|
||||||
SDL_HAPTIC_INFINITY);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::handleTouchFingerEvent(SDL_Window* window, SDL_TouchFingerEvent* event)
|
|
||||||
{
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 10)
|
|
||||||
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
|
|
||||||
// Ignore anything that isn't a touchscreen. We may get callbacks
|
|
||||||
// for trackpads, but we want to handle those in the mouse path.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#elif defined(Q_OS_DARWIN)
|
|
||||||
// SDL2 sends touch events from trackpads by default on
|
|
||||||
// macOS. This totally screws our actual mouse handling,
|
|
||||||
// so we must explicitly ignore touch events on macOS
|
|
||||||
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
|
|
||||||
// to tell them apart.
|
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
|
||||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
|
||||||
// or lower than those values as touch events continue for touches started within the client area that
|
|
||||||
// leave the client area during a drag motion.
|
|
||||||
// dx and dy are deltas from the last touch event, not the first touch down.
|
|
||||||
|
|
||||||
// Ignore touch down events with more than one finger
|
|
||||||
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore touch move and touch up events from the non-primary finger
|
|
||||||
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Rect src, dst;
|
|
||||||
int windowWidth, windowHeight;
|
|
||||||
|
|
||||||
SDL_GetWindowSize(window, &windowWidth, &windowHeight);
|
|
||||||
|
|
||||||
src.x = src.y = 0;
|
|
||||||
src.w = m_StreamWidth;
|
|
||||||
src.h = m_StreamHeight;
|
|
||||||
|
|
||||||
dst.x = dst.y = 0;
|
|
||||||
dst.w = windowWidth;
|
|
||||||
dst.h = windowHeight;
|
|
||||||
|
|
||||||
// Use the stream and window sizes to determine the video region
|
|
||||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
|
||||||
|
|
||||||
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
|
||||||
// Moved too far since touch down. Cancel the long press timer.
|
|
||||||
SDL_RemoveTimer(m_LongPressTimer);
|
|
||||||
m_LongPressTimer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
|
||||||
if (event->type != SDL_FINGERDOWN ||
|
|
||||||
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
|
||||||
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
|
||||||
// Scale window-relative events to be video-relative and clamp to video region
|
|
||||||
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
|
|
||||||
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
|
|
||||||
|
|
||||||
// Update the cursor position relative to the video region
|
|
||||||
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->type == SDL_FINGERDOWN) {
|
|
||||||
m_LastTouchDownEvent = *event;
|
|
||||||
|
|
||||||
// Start/restart the long press timer
|
|
||||||
SDL_RemoveTimer(m_LongPressTimer);
|
|
||||||
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
|
|
||||||
longPressTimerCallback,
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
// Left button down on finger down
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
|
||||||
}
|
|
||||||
else if (event->type == SDL_FINGERUP) {
|
|
||||||
m_LastTouchUpEvent = *event;
|
|
||||||
|
|
||||||
// Cancel the long press timer
|
|
||||||
SDL_RemoveTimer(m_LongPressTimer);
|
|
||||||
m_LongPressTimer = 0;
|
|
||||||
|
|
||||||
// Left button up on finger up
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Raise right button too in case we triggered a long press gesture
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int SdlInputHandler::getAttachedGamepadMask()
|
|
||||||
{
|
|
||||||
int count;
|
|
||||||
int mask;
|
|
||||||
|
|
||||||
if (!m_MultiController) {
|
|
||||||
// Player 1 is always present in non-MC mode
|
|
||||||
return 0x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
count = mask = 0;
|
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
|
||||||
if (SDL_IsGameController(i)) {
|
|
||||||
mask |= (1 << count++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::raiseAllKeys()
|
|
||||||
{
|
|
||||||
if (m_KeysDown.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Raising %d keys",
|
|
||||||
m_KeysDown.count());
|
|
||||||
|
|
||||||
for (auto keyDown : m_KeysDown) {
|
|
||||||
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_KeysDown.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::notifyFocusGained(SDL_Window* window)
|
|
||||||
{
|
|
||||||
// Capture mouse cursor when user actives the window by clicking on
|
|
||||||
// window's client area (borders and title bar excluded).
|
|
||||||
// Without this you would have to click the window twice (once to
|
|
||||||
// activate it, second time to enable capture). With this you need to
|
|
||||||
// click it only once.
|
|
||||||
//
|
|
||||||
// On Linux, the button press event is delivered after the focus gain
|
|
||||||
// so this is not neccessary (and leads to a click sent to the host
|
|
||||||
// when focusing the window by clicking).
|
|
||||||
//
|
|
||||||
// By excluding window's borders and title bar out, lets user still
|
|
||||||
// interact with them without mouse capture kicking in.
|
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
|
||||||
int mouseX, mouseY;
|
|
||||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
|
||||||
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
|
|
||||||
int x, y, width, height;
|
|
||||||
SDL_GetWindowPosition(window, &x, &y);
|
|
||||||
SDL_GetWindowSize(window, &width, &height);
|
|
||||||
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
|
||||||
setCaptureActive(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
Q_UNUSED(window);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::notifyFocusLost(SDL_Window* window)
|
|
||||||
{
|
|
||||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
|
||||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
|
||||||
// Doing this while the window is full-screen breaks the transition out of FS
|
|
||||||
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
|
||||||
if (!(SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) && !m_AbsoluteMouseMode) {
|
|
||||||
setCaptureActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
|
||||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
|
||||||
raiseAllKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SdlInputHandler::isCaptureActive()
|
|
||||||
{
|
|
||||||
if (SDL_GetRelativeMouseMode()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some platforms don't support SDL_SetRelativeMouseMode
|
|
||||||
return m_FakeCaptureActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlInputHandler::setCaptureActive(bool active)
|
|
||||||
{
|
|
||||||
if (active) {
|
|
||||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
|
||||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
|
||||||
// Relative mouse mode didn't work, so we'll use fake capture
|
|
||||||
SDL_ShowCursor(SDL_DISABLE);
|
|
||||||
m_FakeCaptureActive = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (m_FakeCaptureActive) {
|
|
||||||
SDL_ShowCursor(SDL_ENABLE);
|
|
||||||
m_FakeCaptureActive = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SdlInputHandler::getUnmappedGamepads()
|
|
||||||
{
|
|
||||||
QString ret;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
MappingManager mappingManager;
|
|
||||||
mappingManager.applyMappings();
|
|
||||||
|
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
|
||||||
if (!SDL_IsGameController(i)) {
|
|
||||||
char guidStr[33];
|
|
||||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
|
||||||
guidStr, sizeof(guidStr));
|
|
||||||
const char* name = SDL_JoystickNameForIndex(i);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unmapped joystick: %s %s",
|
|
||||||
name ? name : "<UNKNOWN>",
|
|
||||||
guidStr);
|
|
||||||
SDL_Joystick* joy = SDL_JoystickOpen(i);
|
|
||||||
if (joy != nullptr) {
|
|
||||||
int numButtons = SDL_JoystickNumButtons(joy);
|
|
||||||
int numHats = SDL_JoystickNumHats(joy);
|
|
||||||
int numAxes = SDL_JoystickNumAxes(joy);
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
|
||||||
numAxes, numButtons, numHats);
|
|
||||||
|
|
||||||
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Joystick likely to be an unmapped game controller");
|
|
||||||
if (!ret.isEmpty()) {
|
|
||||||
ret += ", ";
|
|
||||||
}
|
|
||||||
|
|
||||||
ret += name;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_JoystickClose(joy);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to open joystick for query: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#include "input.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
// How long the fingers must be stationary to start a right click
|
||||||
|
#define LONG_PRESS_ACTIVATION_DELAY 650
|
||||||
|
|
||||||
|
// How far the finger can move before it cancels a right click
|
||||||
|
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
||||||
|
|
||||||
|
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||||
|
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
|
||||||
|
|
||||||
|
// How far the finger can move before it can override the double tap deadzone
|
||||||
|
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
|
||||||
|
{
|
||||||
|
// Raise the left click and start a right click
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
{
|
||||||
|
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||||
|
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||||
|
// or lower than those values as touch events continue for touches started within the client area that
|
||||||
|
// leave the client area during a drag motion.
|
||||||
|
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||||
|
|
||||||
|
// Ignore touch down events with more than one finger
|
||||||
|
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore touch move and touch up events from the non-primary finger
|
||||||
|
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
int windowWidth, windowHeight;
|
||||||
|
|
||||||
|
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||||
|
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_StreamWidth;
|
||||||
|
src.h = m_StreamHeight;
|
||||||
|
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = windowWidth;
|
||||||
|
dst.h = windowHeight;
|
||||||
|
|
||||||
|
// Use the stream and window sizes to determine the video region
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||||
|
// Moved too far since touch down. Cancel the long press timer.
|
||||||
|
SDL_RemoveTimer(m_LongPressTimer);
|
||||||
|
m_LongPressTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||||
|
if (event->type != SDL_FINGERDOWN ||
|
||||||
|
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||||
|
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||||
|
// Scale window-relative events to be video-relative and clamp to video region
|
||||||
|
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
|
||||||
|
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
|
||||||
|
|
||||||
|
// Update the cursor position relative to the video region
|
||||||
|
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERDOWN) {
|
||||||
|
m_LastTouchDownEvent = *event;
|
||||||
|
|
||||||
|
// Start/restart the long press timer
|
||||||
|
SDL_RemoveTimer(m_LongPressTimer);
|
||||||
|
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
|
||||||
|
longPressTimerCallback,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
// Left button down on finger down
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else if (event->type == SDL_FINGERUP) {
|
||||||
|
m_LastTouchUpEvent = *event;
|
||||||
|
|
||||||
|
// Cancel the long press timer
|
||||||
|
SDL_RemoveTimer(m_LongPressTimer);
|
||||||
|
m_LongPressTimer = 0;
|
||||||
|
|
||||||
|
// Left button up on finger up
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
|
||||||
|
// Raise right button too in case we triggered a long press gesture
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include "settings/mappingmanager.h"
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
// How long the Start button must be pressed to toggle mouse emulation
|
||||||
|
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
|
||||||
|
|
||||||
|
// How long between polling the gamepad to send virtual mouse input
|
||||||
|
#define MOUSE_EMULATION_POLLING_INTERVAL 50
|
||||||
|
|
||||||
|
// Determines how fast the mouse will move each interval
|
||||||
|
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
|
||||||
|
|
||||||
|
// Determines the maximum motion amount before allowing movement
|
||||||
|
#define MOUSE_EMULATION_DEADZONE 2
|
||||||
|
|
||||||
|
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
||||||
|
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||||
|
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||||
|
|
||||||
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
|
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||||
|
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||||
|
LS_CLK_FLAG, RS_CLK_FLAG,
|
||||||
|
LB_FLAG, RB_FLAG,
|
||||||
|
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||||
|
};
|
||||||
|
|
||||||
|
GamepadState*
|
||||||
|
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
|
if (m_GamepadState[i].jsId == id) {
|
||||||
|
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
|
||||||
|
return &m_GamepadState[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should only happen with > 4 gamepads
|
||||||
|
SDL_assert(SDL_NumJoysticks() > 4);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||||
|
{
|
||||||
|
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
|
||||||
|
LiSendMultiControllerEvent(state->index,
|
||||||
|
m_GamepadMask,
|
||||||
|
state->buttons,
|
||||||
|
state->lt,
|
||||||
|
state->rt,
|
||||||
|
state->lsX,
|
||||||
|
state->lsY,
|
||||||
|
state->rsX,
|
||||||
|
state->rsY);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
||||||
|
{
|
||||||
|
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
||||||
|
|
||||||
|
short rawX;
|
||||||
|
short rawY;
|
||||||
|
|
||||||
|
// Determine which analog stick is currently receiving the strongest input
|
||||||
|
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
|
||||||
|
rawX = gamepad->lsX;
|
||||||
|
rawY = -gamepad->lsY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rawX = gamepad->rsX;
|
||||||
|
rawY = -gamepad->rsY;
|
||||||
|
}
|
||||||
|
|
||||||
|
float deltaX;
|
||||||
|
float deltaY;
|
||||||
|
|
||||||
|
// Produce a base vector for mouse movement with increased speed as we deviate further from center
|
||||||
|
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||||
|
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||||
|
|
||||||
|
// Enforce deadzones
|
||||||
|
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
|
||||||
|
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||||
|
{
|
||||||
|
SDL_JoystickID gameControllerId = event->which;
|
||||||
|
GamepadState* state = findStateForGamepad(gameControllerId);
|
||||||
|
if (state == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch all pending axis motion events for this gamepad to save CPU time
|
||||||
|
SDL_Event nextEvent;
|
||||||
|
for (;;) {
|
||||||
|
switch (event->axis)
|
||||||
|
{
|
||||||
|
case SDL_CONTROLLER_AXIS_LEFTX:
|
||||||
|
state->lsX = event->value;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_LEFTY:
|
||||||
|
// 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 = -qMax(event->value, (short)-32767);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
||||||
|
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
||||||
|
state->rt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unhandled controller axis: %d",
|
||||||
|
event->axis);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for another event to batch with
|
||||||
|
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
event = &nextEvent.caxis;
|
||||||
|
if (event->which != gameControllerId) {
|
||||||
|
// Stop batching if a different gamepad interrupts us
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the next event to batch
|
||||||
|
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||||
|
if (state->mouseEmulationTimer == 0) {
|
||||||
|
sendGamepadState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
||||||
|
{
|
||||||
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
|
if (state == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->state == SDL_PRESSED) {
|
||||||
|
state->buttons |= k_ButtonMap[event->button];
|
||||||
|
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||||
|
state->lastStartDownTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
else if (state->mouseEmulationTimer != 0) {
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
|
||||||
|
LiSendScrollEvent(1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
||||||
|
LiSendScrollEvent(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state->buttons &= ~k_ButtonMap[event->button];
|
||||||
|
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||||
|
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
|
||||||
|
if (state->mouseEmulationTimer != 0) {
|
||||||
|
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||||
|
state->mouseEmulationTimer = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Mouse emulation deactivated");
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
}
|
||||||
|
else if (m_GamepadMouse) {
|
||||||
|
// Send the start button up event to the host, since we won't do it below
|
||||||
|
sendGamepadState(state);
|
||||||
|
|
||||||
|
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Mouse emulation active");
|
||||||
|
Session::get()->notifyMouseEmulationMode(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state->mouseEmulationTimer != 0) {
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
||||||
|
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit gamepad button combo");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
|
// Clear buttons down on this gameapd
|
||||||
|
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||||
|
0, 0, 0, 0, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||||
|
if (state->mouseEmulationTimer == 0) {
|
||||||
|
sendGamepadState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
||||||
|
{
|
||||||
|
GamepadState* state;
|
||||||
|
|
||||||
|
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||||
|
int i;
|
||||||
|
const char* name;
|
||||||
|
SDL_GameController* controller;
|
||||||
|
const char* mapping;
|
||||||
|
char guidStr[33];
|
||||||
|
uint32_t hapticCaps;
|
||||||
|
|
||||||
|
controller = SDL_GameControllerOpen(event->which);
|
||||||
|
if (controller == NULL) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open gamepad: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
||||||
|
// can lead to strange issues due to bugs in Windows where an Xbox
|
||||||
|
// controller will join as player 2, even though no player 1 controller
|
||||||
|
// is connected at all. This pretty much screws any attempt to use
|
||||||
|
// the gamepad in single player games, so just assign them in order from 0.
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
for (; i < MAX_GAMEPADS; i++) {
|
||||||
|
SDL_assert(m_GamepadState[i].controller != controller);
|
||||||
|
if (m_GamepadState[i].controller == NULL) {
|
||||||
|
// Found an empty slot
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == MAX_GAMEPADS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No open gamepad slots found!");
|
||||||
|
SDL_GameControllerClose(controller);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = &m_GamepadState[i];
|
||||||
|
if (m_MultiController) {
|
||||||
|
state->index = i;
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 12)
|
||||||
|
// This will change indicators on the controller to show the assigned
|
||||||
|
// player index. For Xbox 360 controllers, that means updating the LED
|
||||||
|
// ring to light up the corresponding quadrant for this player.
|
||||||
|
SDL_GameControllerSetPlayerIndex(controller, state->index);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Always player 1 in single controller mode
|
||||||
|
state->index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->controller = controller;
|
||||||
|
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
// Perform a tiny rumble to see if haptics are supported.
|
||||||
|
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
|
||||||
|
// and we'll get a (potentially false) success value returned.
|
||||||
|
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
|
||||||
|
ML_HAPTIC_GC_RUMBLE : 0;
|
||||||
|
#else
|
||||||
|
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
state->hapticEffectId = -1;
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
// Query for supported haptic effects
|
||||||
|
hapticCaps = SDL_HapticQuery(state->haptic);
|
||||||
|
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
|
||||||
|
ML_HAPTIC_SIMPLE_RUMBLE : 0;
|
||||||
|
|
||||||
|
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
|
||||||
|
if (SDL_HapticRumbleSupported(state->haptic)) {
|
||||||
|
if (SDL_HapticRumbleInit(state->haptic) == 0) {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
state->haptic = nullptr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hapticCaps = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
mapping = SDL_GameControllerMapping(state->controller);
|
||||||
|
name = SDL_GameControllerName(state->controller);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
||||||
|
i,
|
||||||
|
state->index,
|
||||||
|
name != nullptr ? name : "<null>",
|
||||||
|
hapticCaps,
|
||||||
|
guidStr,
|
||||||
|
mapping != nullptr ? mapping : "<null>");
|
||||||
|
if (mapping != nullptr) {
|
||||||
|
SDL_free((void*)mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this gamepad to the gamepad mask
|
||||||
|
if (m_MultiController) {
|
||||||
|
// NB: Don't assert that it's unset here because we will already
|
||||||
|
// have the mask set for initially attached gamepads to avoid confusing
|
||||||
|
// apps running on the host.
|
||||||
|
m_GamepadMask |= (1 << state->index);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_assert(m_GamepadMask == 0x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an empty event to tell the PC we've arrived
|
||||||
|
sendGamepadState(state);
|
||||||
|
}
|
||||||
|
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||||
|
state = findStateForGamepad(event->which);
|
||||||
|
if (state != NULL) {
|
||||||
|
if (state->mouseEmulationTimer != 0) {
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GameControllerClose(state->controller);
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Remove this from the gamepad mask in MC-mode
|
||||||
|
if (m_MultiController) {
|
||||||
|
SDL_assert(m_GamepadMask & (1 << state->index));
|
||||||
|
m_GamepadMask &= ~(1 << state->index);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_assert(m_GamepadMask == 0x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Gamepad %d is gone",
|
||||||
|
state->index);
|
||||||
|
|
||||||
|
// Send a final event to let the PC know this gamepad is gone
|
||||||
|
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||||
|
0, 0, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Clear all remaining state from this slot
|
||||||
|
SDL_memset(state, 0, sizeof(*state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
||||||
|
{
|
||||||
|
SDL_assert(event->type == SDL_JOYDEVICEADDED);
|
||||||
|
|
||||||
|
if (!SDL_IsGameController(event->which)) {
|
||||||
|
char guidStr[33];
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
const char* name = SDL_JoystickNameForIndex(event->which);
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Joystick discovered with no mapping: %s %s",
|
||||||
|
name ? name : "<UNKNOWN>",
|
||||||
|
guidStr);
|
||||||
|
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
|
||||||
|
if (joy != nullptr) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||||
|
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
|
||||||
|
SDL_JoystickNumHats(joy));
|
||||||
|
SDL_JoystickClose(joy);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open joystick for query: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||||
|
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Check if the controller supports haptics (and if the controller exists at all)
|
||||||
|
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
|
||||||
|
if (haptic == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the last effect we played
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumbleStop(haptic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this callback is telling us to stop both motors, don't bother queuing a new effect
|
||||||
|
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
SDL_HapticEffect effect;
|
||||||
|
SDL_memset(&effect, 0, sizeof(effect));
|
||||||
|
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
|
||||||
|
// The effect should last until we are instructed to stop or change it
|
||||||
|
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
||||||
|
|
||||||
|
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
|
||||||
|
effect.leftright.large_magnitude = lowFreqMotor / 2;
|
||||||
|
effect.leftright.small_magnitude = highFreqMotor / 2;
|
||||||
|
|
||||||
|
// Play the new effect
|
||||||
|
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumblePlay(haptic,
|
||||||
|
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
|
||||||
|
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
|
||||||
|
SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SdlInputHandler::getUnmappedGamepads()
|
||||||
|
{
|
||||||
|
QString ret;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
if (!SDL_IsGameController(i)) {
|
||||||
|
char guidStr[33];
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
const char* name = SDL_JoystickNameForIndex(i);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unmapped joystick: %s %s",
|
||||||
|
name ? name : "<UNKNOWN>",
|
||||||
|
guidStr);
|
||||||
|
SDL_Joystick* joy = SDL_JoystickOpen(i);
|
||||||
|
if (joy != nullptr) {
|
||||||
|
int numButtons = SDL_JoystickNumButtons(joy);
|
||||||
|
int numHats = SDL_JoystickNumHats(joy);
|
||||||
|
int numAxes = SDL_JoystickNumAxes(joy);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||||
|
numAxes, numButtons, numHats);
|
||||||
|
|
||||||
|
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Joystick likely to be an unmapped game controller");
|
||||||
|
if (!ret.isEmpty()) {
|
||||||
|
ret += ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += name;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_JoystickClose(joy);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open joystick for query: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SdlInputHandler::getAttachedGamepadMask()
|
||||||
|
{
|
||||||
|
int count;
|
||||||
|
int mask;
|
||||||
|
|
||||||
|
if (!m_MultiController) {
|
||||||
|
// Player 1 is always present in non-MC mode
|
||||||
|
return 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
count = mask = 0;
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
if (SDL_IsGameController(i)) {
|
||||||
|
mask |= (1 << count++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include "streaming/session.h"
|
||||||
|
#include "settings/mappingmanager.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
#define MOUSE_POLLING_INTERVAL 5
|
||||||
|
|
||||||
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||||
|
: m_MultiController(prefs.multiController),
|
||||||
|
m_GamepadMouse(prefs.gamepadMouse),
|
||||||
|
m_SwapMouseButtons(prefs.swapMouseButtons),
|
||||||
|
m_MouseMoveTimer(0),
|
||||||
|
m_MousePositionLock(0),
|
||||||
|
m_MouseWasInVideoRegion(false),
|
||||||
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||||
|
m_FakeCaptureActive(false),
|
||||||
|
m_LongPressTimer(0),
|
||||||
|
m_StreamWidth(streamWidth),
|
||||||
|
m_StreamHeight(streamHeight),
|
||||||
|
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||||
|
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||||
|
m_PendingMouseLeaveButtonUp(0),
|
||||||
|
m_LeftButtonReleaseTimer(0),
|
||||||
|
m_RightButtonReleaseTimer(0),
|
||||||
|
m_DragTimer(0),
|
||||||
|
m_DragButton(0),
|
||||||
|
m_NumFingersDown(0)
|
||||||
|
{
|
||||||
|
// Allow gamepad input when the app doesn't have focus
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
|
||||||
|
// If absolute mouse mode is enabled, use relative mode warp (which
|
||||||
|
// is via normal motion events that are influenced by mouse acceleration).
|
||||||
|
// Otherwise, we'll use raw input capture which is straight from the device
|
||||||
|
// without modification by the OS.
|
||||||
|
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||||
|
prefs.absoluteMouseMode ? "1" : "0",
|
||||||
|
SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
|
// Allow clicks to pass through to us when focusing the window. If we're in
|
||||||
|
// absolute mouse mode, this will avoid the user having to click twice to
|
||||||
|
// trigger a click on the host if the Moonlight window is not focused. In
|
||||||
|
// relative mode, the click event will trigger the mouse to be recaptured.
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||||
|
|
||||||
|
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
|
||||||
|
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
|
||||||
|
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
|
||||||
|
// SDL 2.0.10 where the bug is fixed.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/133
|
||||||
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
// Enabling extended input reports allows rumble to function on Bluetooth PS4
|
||||||
|
// controllers, but breaks DirectInput applications. We will enable it because
|
||||||
|
// it's likely that working rumble is what the user is expecting. If they don't
|
||||||
|
// want this behavior, they can override it with the environment variable.
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||||
|
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||||
|
|
||||||
|
QString streamIgnoreDevices = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES");
|
||||||
|
QString streamIgnoreDevicesExcept = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT");
|
||||||
|
|
||||||
|
if (!streamIgnoreDevices.isEmpty() && !streamIgnoreDevices.endsWith(',')) {
|
||||||
|
streamIgnoreDevices += ',';
|
||||||
|
}
|
||||||
|
streamIgnoreDevices += m_OldIgnoreDevices;
|
||||||
|
|
||||||
|
// For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES
|
||||||
|
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
|
||||||
|
// overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming.
|
||||||
|
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, streamIgnoreDevices.toUtf8());
|
||||||
|
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, streamIgnoreDevicesExcept.toUtf8());
|
||||||
|
|
||||||
|
// We must initialize joystick explicitly before gamecontroller in order
|
||||||
|
// to ensure we receive gamecontroller attach events for gamepads where
|
||||||
|
// SDL doesn't have a built-in mapping. By starting joystick first, we
|
||||||
|
// can allow mapping manager to update the mappings before GC attach
|
||||||
|
// events are generated.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
|
// Flush gamepad arrival and departure events which may be queued before
|
||||||
|
// starting the gamecontroller subsystem again. This prevents us from
|
||||||
|
// receiving duplicate arrival and departure events for the same gamepad.
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
|
||||||
|
|
||||||
|
// We need to reinit this each time, since you only get
|
||||||
|
// an initial set of gamepad arrival events once per init.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||||
|
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||||
|
// during stream startup as we detect currently attached gamepads one at a time.
|
||||||
|
m_GamepadMask = getAttachedGamepadMask();
|
||||||
|
|
||||||
|
SDL_zero(m_GamepadState);
|
||||||
|
SDL_zero(m_LastTouchDownEvent);
|
||||||
|
SDL_zero(m_LastTouchUpEvent);
|
||||||
|
SDL_zero(m_TouchDownEvent);
|
||||||
|
SDL_zero(m_MousePositionReport);
|
||||||
|
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
||||||
|
SDL_AtomicSet(&m_MousePositionUpdated, 0);
|
||||||
|
|
||||||
|
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
|
||||||
|
if (pollingInterval == 0) {
|
||||||
|
pollingInterval = MOUSE_POLLING_INTERVAL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using custom mouse polling interval: %u ms",
|
||||||
|
pollingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlInputHandler::~SdlInputHandler()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
|
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
|
||||||
|
}
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (m_GamepadState[i].haptic != nullptr) {
|
||||||
|
SDL_HapticClose(m_GamepadState[i].haptic);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (m_GamepadState[i].controller != nullptr) {
|
||||||
|
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RemoveTimer(m_MouseMoveTimer);
|
||||||
|
SDL_RemoveTimer(m_LongPressTimer);
|
||||||
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
|
||||||
|
// Return background event handling to off
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
|
||||||
|
|
||||||
|
// Restore the ignored devices
|
||||||
|
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, m_OldIgnoreDevices.toUtf8());
|
||||||
|
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, m_OldIgnoreDevicesExcept.toUtf8());
|
||||||
|
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
// Hide SDL's cursor on Steam Link after quitting the stream.
|
||||||
|
// FIXME: We should also do this for other situations where SDL
|
||||||
|
// and Qt will draw their own mouse cursors like KMSDRM or RPi
|
||||||
|
// video backends.
|
||||||
|
SDL_ShowCursor(SDL_DISABLE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::setWindow(SDL_Window *window)
|
||||||
|
{
|
||||||
|
m_Window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::raiseAllKeys()
|
||||||
|
{
|
||||||
|
if (m_KeysDown.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Raising %d keys",
|
||||||
|
m_KeysDown.count());
|
||||||
|
|
||||||
|
for (auto keyDown : m_KeysDown) {
|
||||||
|
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_KeysDown.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::notifyMouseLeave()
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
||||||
|
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
|
||||||
|
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||||
|
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||||
|
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||||
|
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||||
|
if (mouseState & SDL_BUTTON(button)) {
|
||||||
|
m_PendingMouseLeaveButtonUp = button;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::notifyFocusLost()
|
||||||
|
{
|
||||||
|
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||||
|
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||||
|
// Doing this while the window is full-screen breaks the transition out of FS
|
||||||
|
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||||
|
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) && !m_AbsoluteMouseMode) {
|
||||||
|
setCaptureActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||||
|
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||||
|
raiseAllKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlInputHandler::isCaptureActive()
|
||||||
|
{
|
||||||
|
if (SDL_GetRelativeMouseMode()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some platforms don't support SDL_SetRelativeMouseMode
|
||||||
|
return m_FakeCaptureActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::setCaptureActive(bool active)
|
||||||
|
{
|
||||||
|
if (active) {
|
||||||
|
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||||
|
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_AbsoluteMouseMode) {
|
||||||
|
// If our window is occluded when mouse is captured, the mouse may
|
||||||
|
// get stuck on top of the occluding window and not be properly
|
||||||
|
// captured. We can avoid this by raising our window before we
|
||||||
|
// capture the mouse.
|
||||||
|
SDL_RaiseWindow(m_Window);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||||
|
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||||
|
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||||
|
SDL_ShowCursor(SDL_DISABLE);
|
||||||
|
m_FakeCaptureActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronize the client and host cursor when activating absolute capture
|
||||||
|
if (m_AbsoluteMouseMode) {
|
||||||
|
int mouseX, mouseY;
|
||||||
|
int windowX, windowY;
|
||||||
|
|
||||||
|
// We have to use SDL_GetGlobalMouseState() because macOS may not reflect
|
||||||
|
// the new position of the mouse when outside the window.
|
||||||
|
SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||||
|
|
||||||
|
// Convert global mouse state to window-relative
|
||||||
|
SDL_GetWindowPosition(m_Window, &windowX, &windowY);
|
||||||
|
mouseX -= windowX;
|
||||||
|
mouseY -= windowY;
|
||||||
|
|
||||||
|
if (isMouseInVideoRegion(mouseX, mouseY)) {
|
||||||
|
updateMousePositionReport(mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (m_FakeCaptureActive) {
|
||||||
|
// Display the cursor again
|
||||||
|
SDL_ShowCursor(SDL_ENABLE);
|
||||||
|
m_FakeCaptureActive = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the cursor to leave the bounds of our window again.
|
||||||
|
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
{
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 10)
|
||||||
|
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
|
||||||
|
// Ignore anything that isn't a touchscreen. We may get callbacks
|
||||||
|
// for trackpads, but we want to handle those in the mouse path.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#elif defined(Q_OS_DARWIN)
|
||||||
|
// SDL2 sends touch events from trackpads by default on
|
||||||
|
// macOS. This totally screws our actual mouse handling,
|
||||||
|
// so we must explicitly ignore touch events on macOS
|
||||||
|
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
|
||||||
|
// to tell them apart.
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m_AbsoluteTouchMode) {
|
||||||
|
handleAbsoluteFingerEvent(event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
handleRelativeFingerEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#define SDL_CODE_HIDE_CURSOR 1
|
||||||
|
#define SDL_CODE_SHOW_CURSOR 2
|
||||||
|
|
||||||
struct GamepadState {
|
struct GamepadState {
|
||||||
SDL_GameController* controller;
|
SDL_GameController* controller;
|
||||||
SDL_JoystickID jsId;
|
SDL_JoystickID jsId;
|
||||||
@@ -43,11 +46,13 @@ public:
|
|||||||
|
|
||||||
~SdlInputHandler();
|
~SdlInputHandler();
|
||||||
|
|
||||||
|
void setWindow(SDL_Window* window);
|
||||||
|
|
||||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||||
|
|
||||||
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
||||||
|
|
||||||
void handleMouseMotionEvent(SDL_Window* window, SDL_MouseMotionEvent* event);
|
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
|
||||||
|
|
||||||
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
||||||
|
|
||||||
@@ -61,20 +66,26 @@ public:
|
|||||||
|
|
||||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
void handleTouchFingerEvent(SDL_Window* window, SDL_TouchFingerEvent* event);
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
int getAttachedGamepadMask();
|
int getAttachedGamepadMask();
|
||||||
|
|
||||||
void raiseAllKeys();
|
void raiseAllKeys();
|
||||||
|
|
||||||
void notifyFocusGained(SDL_Window* window);
|
void notifyMouseLeave();
|
||||||
|
|
||||||
void notifyFocusLost(SDL_Window* window);
|
void notifyFocusLost();
|
||||||
|
|
||||||
bool isCaptureActive();
|
bool isCaptureActive();
|
||||||
|
|
||||||
void setCaptureActive(bool active);
|
void setCaptureActive(bool active);
|
||||||
|
|
||||||
|
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
|
||||||
|
|
||||||
|
void updateMousePositionReport(int mouseX, int mouseY);
|
||||||
|
|
||||||
|
void flushMousePositionUpdate();
|
||||||
|
|
||||||
static
|
static
|
||||||
QString getUnmappedGamepads();
|
QString getUnmappedGamepads();
|
||||||
|
|
||||||
@@ -84,6 +95,10 @@ private:
|
|||||||
|
|
||||||
void sendGamepadState(GamepadState* state);
|
void sendGamepadState(GamepadState* state);
|
||||||
|
|
||||||
|
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
|
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
static
|
static
|
||||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
@@ -93,15 +108,38 @@ private:
|
|||||||
static
|
static
|
||||||
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
SDL_Window* m_Window;
|
||||||
bool m_MultiController;
|
bool m_MultiController;
|
||||||
bool m_GamepadMouse;
|
bool m_GamepadMouse;
|
||||||
|
bool m_SwapMouseButtons;
|
||||||
SDL_TimerID m_MouseMoveTimer;
|
SDL_TimerID m_MouseMoveTimer;
|
||||||
SDL_atomic_t m_MouseDeltaX;
|
SDL_atomic_t m_MouseDeltaX;
|
||||||
SDL_atomic_t m_MouseDeltaY;
|
SDL_atomic_t m_MouseDeltaY;
|
||||||
|
|
||||||
|
SDL_SpinLock m_MousePositionLock;
|
||||||
|
struct {
|
||||||
|
int x, y;
|
||||||
|
int windowWidth, windowHeight;
|
||||||
|
} m_MousePositionReport;
|
||||||
|
SDL_atomic_t m_MousePositionUpdated;
|
||||||
|
bool m_MouseWasInVideoRegion;
|
||||||
|
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
|
||||||
|
|
||||||
int m_GamepadMask;
|
int m_GamepadMask;
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||||
QSet<short> m_KeysDown;
|
QSet<short> m_KeysDown;
|
||||||
bool m_FakeCaptureActive;
|
bool m_FakeCaptureActive;
|
||||||
|
QString m_OldIgnoreDevices;
|
||||||
|
QString m_OldIgnoreDevicesExcept;
|
||||||
|
|
||||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||||
@@ -109,6 +147,15 @@ private:
|
|||||||
int m_StreamWidth;
|
int m_StreamWidth;
|
||||||
int m_StreamHeight;
|
int m_StreamHeight;
|
||||||
bool m_AbsoluteMouseMode;
|
bool m_AbsoluteMouseMode;
|
||||||
|
bool m_AbsoluteTouchMode;
|
||||||
|
Uint32 m_PendingMouseLeaveButtonUp;
|
||||||
|
|
||||||
|
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||||
|
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||||
|
SDL_TimerID m_RightButtonReleaseTimer;
|
||||||
|
SDL_TimerID m_DragTimer;
|
||||||
|
char m_DragButton;
|
||||||
|
int m_NumFingersDown;
|
||||||
|
|
||||||
static const int k_ButtonMap[];
|
static const int k_ButtonMap[];
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
// Until we can fully capture these on all platforms (without conflicting with
|
||||||
|
// OS-provided shortcuts), we should avoid passing them through to the host.
|
||||||
|
//#define ENABLE_META
|
||||||
|
|
||||||
|
#define VK_0 0x30
|
||||||
|
#define VK_A 0x41
|
||||||
|
|
||||||
|
// These are real Windows VK_* codes
|
||||||
|
#ifndef VK_F1
|
||||||
|
#define VK_F1 0x70
|
||||||
|
#define VK_F13 0x7C
|
||||||
|
#define VK_NUMPAD0 0x60
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
|
{
|
||||||
|
short keyCode;
|
||||||
|
char modifiers;
|
||||||
|
|
||||||
|
// Check for our special key combos
|
||||||
|
if ((event->state == SDL_PRESSED) &&
|
||||||
|
(event->keysym.mod & KMOD_CTRL) &&
|
||||||
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
|
// First we test the SDLK combos for matches,
|
||||||
|
// that way we ensure that latin keyboard users
|
||||||
|
// can match to the key they see on their keyboards.
|
||||||
|
// If nothing matches that, we'll then go on to
|
||||||
|
// checking scancodes so non-latin keyboard users
|
||||||
|
// can have working hotkeys (though possibly in
|
||||||
|
// odd positions). We must do all SDLK tests before
|
||||||
|
// any scancode tests to avoid issues in cases
|
||||||
|
// where the SDLK for one shortcut collides with
|
||||||
|
// the scancode of another.
|
||||||
|
|
||||||
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
|
if (event->keysym.sym == SDLK_q) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit key combo (SDLK)");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
||||||
|
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse capture toggle combo (SDLK)");
|
||||||
|
|
||||||
|
// Stop handling future input
|
||||||
|
setCaptureActive(!isCaptureActive());
|
||||||
|
|
||||||
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
|
// since we won't get their key up events.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||||
|
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse mode toggle combo (SDLK)");
|
||||||
|
|
||||||
|
// Uncapture input
|
||||||
|
setCaptureActive(false);
|
||||||
|
|
||||||
|
// Toggle mouse mode
|
||||||
|
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||||
|
|
||||||
|
// Recapture input
|
||||||
|
setCaptureActive(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected full-screen toggle combo (SDLK)");
|
||||||
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
||||||
|
else if (event->keysym.sym == SDLK_s) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle combo (SDLK)");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit key combo (scancode)");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse capture toggle combo (scancode)");
|
||||||
|
|
||||||
|
// Stop handling future input
|
||||||
|
setCaptureActive(!isCaptureActive());
|
||||||
|
|
||||||
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
|
// since we won't get their key up events.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected full-screen toggle combo (scancode)");
|
||||||
|
Session::s_ActiveSession->toggleFullscreen();
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse mode toggle combo (scancode)");
|
||||||
|
|
||||||
|
// Uncapture input
|
||||||
|
setCaptureActive(false);
|
||||||
|
|
||||||
|
// Toggle mouse mode
|
||||||
|
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||||
|
|
||||||
|
// Recapture input
|
||||||
|
setCaptureActive(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle combo (scancode)");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
|
||||||
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
|
// transition just in case key events get lost.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->repeat) {
|
||||||
|
// Ignore repeat key down events
|
||||||
|
SDL_assert(event->state == SDL_PRESSED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set modifier flags
|
||||||
|
modifiers = 0;
|
||||||
|
if (event->keysym.mod & KMOD_CTRL) {
|
||||||
|
modifiers |= MODIFIER_CTRL;
|
||||||
|
}
|
||||||
|
if (event->keysym.mod & KMOD_ALT) {
|
||||||
|
modifiers |= MODIFIER_ALT;
|
||||||
|
}
|
||||||
|
if (event->keysym.mod & KMOD_SHIFT) {
|
||||||
|
modifiers |= MODIFIER_SHIFT;
|
||||||
|
}
|
||||||
|
#ifdef ENABLE_META
|
||||||
|
if (event->keysym.mod & KMOD_GUI) {
|
||||||
|
modifiers |= MODIFIER_META;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
||||||
|
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
||||||
|
// a QWERTY layout to work.
|
||||||
|
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
|
||||||
|
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
|
||||||
|
}
|
||||||
|
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
|
||||||
|
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
|
||||||
|
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
switch (event->keysym.scancode) {
|
||||||
|
case SDL_SCANCODE_BACKSPACE:
|
||||||
|
keyCode = 0x08;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_TAB:
|
||||||
|
keyCode = 0x09;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_CLEAR:
|
||||||
|
keyCode = 0x0C;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
|
||||||
|
case SDL_SCANCODE_RETURN:
|
||||||
|
keyCode = 0x0D;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_PAUSE:
|
||||||
|
keyCode = 0x13;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_CAPSLOCK:
|
||||||
|
keyCode = 0x14;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_ESCAPE:
|
||||||
|
keyCode = 0x1B;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_SPACE:
|
||||||
|
keyCode = 0x20;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_PAGEUP:
|
||||||
|
keyCode = 0x21;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_PAGEDOWN:
|
||||||
|
keyCode = 0x22;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_END:
|
||||||
|
keyCode = 0x23;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_HOME:
|
||||||
|
keyCode = 0x24;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_LEFT:
|
||||||
|
keyCode = 0x25;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_UP:
|
||||||
|
keyCode = 0x26;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RIGHT:
|
||||||
|
keyCode = 0x27;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_DOWN:
|
||||||
|
keyCode = 0x28;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_SELECT:
|
||||||
|
keyCode = 0x29;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_EXECUTE:
|
||||||
|
keyCode = 0x2B;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_PRINTSCREEN:
|
||||||
|
keyCode = 0x2C;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_INSERT:
|
||||||
|
keyCode = 0x2D;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_DELETE:
|
||||||
|
keyCode = 0x2E;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_HELP:
|
||||||
|
keyCode = 0x2F;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_0:
|
||||||
|
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
|
||||||
|
keyCode = VK_NUMPAD0;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_0:
|
||||||
|
// See comment above about why we only handle SDL_SCANCODE_0 here
|
||||||
|
keyCode = VK_0;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_MULTIPLY:
|
||||||
|
keyCode = 0x6A;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_PLUS:
|
||||||
|
keyCode = 0x6B;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_COMMA:
|
||||||
|
keyCode = 0x6C;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_MINUS:
|
||||||
|
keyCode = 0x6D;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_PERIOD:
|
||||||
|
keyCode = 0x6E;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_KP_DIVIDE:
|
||||||
|
keyCode = 0x6F;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||||
|
keyCode = 0x90;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_SCROLLLOCK:
|
||||||
|
keyCode = 0x91;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_LSHIFT:
|
||||||
|
keyCode = 0xA0;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RSHIFT:
|
||||||
|
keyCode = 0xA1;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_LCTRL:
|
||||||
|
keyCode = 0xA2;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RCTRL:
|
||||||
|
keyCode = 0xA3;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_LALT:
|
||||||
|
keyCode = 0xA4;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RALT:
|
||||||
|
keyCode = 0xA5;
|
||||||
|
break;
|
||||||
|
#ifdef ENABLE_META
|
||||||
|
case SDL_SCANCODE_LGUI:
|
||||||
|
keyCode = 0x5B;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RGUI:
|
||||||
|
keyCode = 0x5C;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case SDL_SCANCODE_AC_BACK:
|
||||||
|
keyCode = 0xA6;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_FORWARD:
|
||||||
|
keyCode = 0xA7;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_REFRESH:
|
||||||
|
keyCode = 0xA8;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_STOP:
|
||||||
|
keyCode = 0xA9;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_SEARCH:
|
||||||
|
keyCode = 0xAA;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_BOOKMARKS:
|
||||||
|
keyCode = 0xAB;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_AC_HOME:
|
||||||
|
keyCode = 0xAC;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_SEMICOLON:
|
||||||
|
keyCode = 0xBA;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_EQUALS:
|
||||||
|
keyCode = 0xBB;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_COMMA:
|
||||||
|
keyCode = 0xBC;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_MINUS:
|
||||||
|
keyCode = 0xBD;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_PERIOD:
|
||||||
|
keyCode = 0xBE;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_SLASH:
|
||||||
|
keyCode = 0xBF;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_GRAVE:
|
||||||
|
keyCode = 0xC0;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_LEFTBRACKET:
|
||||||
|
keyCode = 0xDB;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_BACKSLASH:
|
||||||
|
keyCode = 0xDC;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_RIGHTBRACKET:
|
||||||
|
keyCode = 0xDD;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_APOSTROPHE:
|
||||||
|
keyCode = 0xDE;
|
||||||
|
break;
|
||||||
|
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||||
|
keyCode = 0xE2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unhandled button event: %d",
|
||||||
|
event->keysym.scancode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the key state so we always know which keys are down
|
||||||
|
if (event->state == SDL_PRESSED) {
|
||||||
|
m_KeysDown.insert(keyCode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_KeysDown.remove(keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
LiSendKeyboardEvent(keyCode,
|
||||||
|
event->state == SDL_PRESSED ?
|
||||||
|
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||||
|
modifiers);
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
#include "input.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
||||||
|
{
|
||||||
|
int button;
|
||||||
|
|
||||||
|
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (!isCaptureActive()) {
|
||||||
|
if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED &&
|
||||||
|
isMouseInVideoRegion(event->x, event->y)) {
|
||||||
|
// Capture the mouse again if clicked when unbound.
|
||||||
|
// We start capture on left button released instead of
|
||||||
|
// pressed to avoid sending an errant mouse button released
|
||||||
|
// event to the host when clicking into our window (since
|
||||||
|
// the pressed event was consumed by this code).
|
||||||
|
setCaptureActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not capturing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (m_AbsoluteMouseMode && !isMouseInVideoRegion(event->x, event->y) && event->state == SDL_PRESSED) {
|
||||||
|
// Ignore button presses outside the video region, but allow button releases
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->button)
|
||||||
|
{
|
||||||
|
case SDL_BUTTON_LEFT:
|
||||||
|
button = BUTTON_LEFT;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_MIDDLE:
|
||||||
|
button = BUTTON_MIDDLE;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_RIGHT:
|
||||||
|
button = BUTTON_RIGHT;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_X1:
|
||||||
|
button = BUTTON_X1;
|
||||||
|
break;
|
||||||
|
case SDL_BUTTON_X2:
|
||||||
|
button = BUTTON_X2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unhandled button event: %d",
|
||||||
|
event->button);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_SwapMouseButtons) {
|
||||||
|
if (button == BUTTON_RIGHT)
|
||||||
|
button = BUTTON_LEFT;
|
||||||
|
else if (button == BUTTON_LEFT)
|
||||||
|
button = BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure any pending mouse position update has been sent before we send
|
||||||
|
// our button event. This ensures that the cursor is in the correct location
|
||||||
|
// when the button event is issued.
|
||||||
|
//
|
||||||
|
// On platforms like macOS, the mouse doesn't track when the window isn't
|
||||||
|
// focused. When we gain focus via mouse click, we immediately get a mouse
|
||||||
|
// move event and a mouse button event. If we don't flush here, the button
|
||||||
|
// will probably arrive before the mouse timer issues the position update.
|
||||||
|
flushMousePositionUpdate();
|
||||||
|
|
||||||
|
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
|
||||||
|
BUTTON_ACTION_PRESS :
|
||||||
|
BUTTON_ACTION_RELEASE,
|
||||||
|
button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::updateMousePositionReport(int mouseX, int mouseY)
|
||||||
|
{
|
||||||
|
int windowWidth, windowHeight;
|
||||||
|
|
||||||
|
// Call SDL_GetWindowSize() before entering the spinlock
|
||||||
|
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||||
|
|
||||||
|
SDL_AtomicLock(&m_MousePositionLock);
|
||||||
|
m_MousePositionReport.x = mouseX;
|
||||||
|
m_MousePositionReport.y = mouseY;
|
||||||
|
m_MousePositionReport.windowWidth = windowWidth;
|
||||||
|
m_MousePositionReport.windowHeight = windowHeight;
|
||||||
|
SDL_AtomicUnlock(&m_MousePositionLock);
|
||||||
|
SDL_AtomicSet(&m_MousePositionUpdated, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::flushMousePositionUpdate()
|
||||||
|
{
|
||||||
|
bool hasNewPosition = SDL_AtomicSet(&m_MousePositionUpdated, 0) != 0;
|
||||||
|
if (hasNewPosition) {
|
||||||
|
// If the lock is held now, the main thread is trying to update
|
||||||
|
// the mouse position. We'll pick up the new position next time.
|
||||||
|
if (SDL_AtomicTryLock(&m_MousePositionLock)) {
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
bool mouseInVideoRegion;
|
||||||
|
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_StreamWidth;
|
||||||
|
src.h = m_StreamHeight;
|
||||||
|
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = m_MousePositionReport.windowWidth;
|
||||||
|
dst.h = m_MousePositionReport.windowHeight;
|
||||||
|
|
||||||
|
// Use the stream and window sizes to determine the video region
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
mouseInVideoRegion = isMouseInVideoRegion(m_MousePositionReport.x,
|
||||||
|
m_MousePositionReport.y,
|
||||||
|
m_MousePositionReport.windowWidth,
|
||||||
|
m_MousePositionReport.windowHeight);
|
||||||
|
|
||||||
|
// Clamp motion to the video region
|
||||||
|
short x = qMin(qMax(m_MousePositionReport.x - dst.x, 0), dst.w);
|
||||||
|
short y = qMin(qMax(m_MousePositionReport.y - dst.y, 0), dst.h);
|
||||||
|
|
||||||
|
// Release the spinlock to unblock the main thread
|
||||||
|
SDL_AtomicUnlock(&m_MousePositionLock);
|
||||||
|
|
||||||
|
// Send the mouse position update if one of the following is true:
|
||||||
|
// a) it is in the video region now
|
||||||
|
// b) it just left the video region (to ensure the mouse is clamped to the video boundary)
|
||||||
|
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
||||||
|
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||||
|
if (buttonState == 0) {
|
||||||
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||||
|
}
|
||||||
|
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||||
|
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust the cursor visibility if applicable
|
||||||
|
if (mouseInVideoRegion ^ m_MouseWasInVideoRegion) {
|
||||||
|
// We must push an event for the main thread to process, because it's not safe
|
||||||
|
// to directly can SDL_ShowCursor() on the arbitrary thread on which this timer
|
||||||
|
// executes.
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_USEREVENT;
|
||||||
|
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
|
if (!mouseInVideoRegion && buttonState != 0) {
|
||||||
|
// If we still have a button pressed on leave, wait for that to come up
|
||||||
|
// before we stop sending mouse position events.
|
||||||
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_MouseWasInVideoRegion = mouseInVideoRegion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
||||||
|
{
|
||||||
|
if (!isCaptureActive()) {
|
||||||
|
// Not capturing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch until the next mouse polling window or we'll get awful
|
||||||
|
// input lag everything except GFE 3.14 and 3.15.
|
||||||
|
if (m_AbsoluteMouseMode) {
|
||||||
|
updateMousePositionReport(event->x, event->y);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
|
||||||
|
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||||
|
{
|
||||||
|
if (!isCaptureActive()) {
|
||||||
|
// Not capturing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_AbsoluteMouseMode) {
|
||||||
|
int mouseX, mouseY;
|
||||||
|
SDL_GetMouseState(&mouseX, &mouseY);
|
||||||
|
if (!isMouseInVideoRegion(mouseX, mouseY)) {
|
||||||
|
// Ignore scroll events outside the video region
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->y != 0) {
|
||||||
|
LiSendScrollEvent((signed char)event->y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlInputHandler::isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth, int windowHeight)
|
||||||
|
{
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
|
||||||
|
if (windowWidth < 0 || windowHeight < 0) {
|
||||||
|
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_StreamWidth;
|
||||||
|
src.h = m_StreamHeight;
|
||||||
|
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = windowWidth;
|
||||||
|
dst.h = windowHeight;
|
||||||
|
|
||||||
|
// Use the stream and window sizes to determine the video region
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
return (mouseX >= dst.x && mouseX <= dst.x + dst.w) &&
|
||||||
|
(mouseY >= dst.y && mouseY <= dst.y + dst.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||||
|
|
||||||
|
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
|
||||||
|
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send mouse position updates if applicable
|
||||||
|
me->flushMousePositionUpdate();
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// See comment in SdlInputHandler::notifyMouseLeave()
|
||||||
|
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
|
||||||
|
int mouseX, mouseY;
|
||||||
|
int windowX, windowY;
|
||||||
|
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||||
|
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
|
||||||
|
|
||||||
|
// If the button is now up, send the synthetic mouse up event
|
||||||
|
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
event.button.type = SDL_MOUSEBUTTONUP;
|
||||||
|
event.button.timestamp = SDL_GetTicks();
|
||||||
|
event.button.windowID = SDL_GetWindowID(me->m_Window);
|
||||||
|
event.button.which = 0;
|
||||||
|
event.button.button = me->m_PendingMouseLeaveButtonUp;
|
||||||
|
event.button.state = SDL_RELEASED;
|
||||||
|
event.button.clicks = 1;
|
||||||
|
event.button.x = mouseX - windowX;
|
||||||
|
event.button.y = mouseY - windowY;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
|
me->m_PendingMouseLeaveButtonUp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include "input.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
// How long the mouse button will be pressed for a tap to click gesture
|
||||||
|
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||||
|
|
||||||
|
// How long the fingers must be stationary to start a drag
|
||||||
|
#define DRAG_ACTIVATION_DELAY 650
|
||||||
|
|
||||||
|
// How far the finger can move before it cancels a drag or tap
|
||||||
|
#define DEAD_ZONE_DELTA 0.01f
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
|
||||||
|
{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
|
||||||
|
{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||||
|
|
||||||
|
// Check how many fingers are down now to decide
|
||||||
|
// which button to hold down
|
||||||
|
if (me->m_NumFingersDown == 2) {
|
||||||
|
me->m_DragButton = BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
else if (me->m_NumFingersDown == 1) {
|
||||||
|
me->m_DragButton = BUTTON_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
{
|
||||||
|
int fingerIndex = -1;
|
||||||
|
|
||||||
|
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||||
|
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||||
|
// or lower than those values as touch events continue for touches started within the client area that
|
||||||
|
// leave the client area during a drag motion.
|
||||||
|
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||||
|
|
||||||
|
// Determine the index of this finger using our list
|
||||||
|
// of fingers that are currently active on screen.
|
||||||
|
// This is also required to handle finger up which
|
||||||
|
// where the finger will not be in SDL_GetTouchFinger()
|
||||||
|
// anymore.
|
||||||
|
if (event->type != SDL_FINGERDOWN) {
|
||||||
|
for (int i = 0; i < MAX_FINGERS; i++) {
|
||||||
|
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
|
||||||
|
fingerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Resolve the new finger by determining the ID of each
|
||||||
|
// finger on the display.
|
||||||
|
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
|
||||||
|
for (int i = 0; i < numTouchFingers; i++) {
|
||||||
|
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
|
||||||
|
SDL_assert(finger != nullptr);
|
||||||
|
if (finger != nullptr) {
|
||||||
|
if (finger->id == event->fingerId) {
|
||||||
|
fingerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
|
||||||
|
// Too many fingers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor motion based on the position of the
|
||||||
|
// primary finger on screen
|
||||||
|
if (fingerIndex == 0) {
|
||||||
|
// The event x and y values are relative to our window width
|
||||||
|
// and height. However, we want to scale them to be relative
|
||||||
|
// to the host resolution. Fortunately this is easy since we
|
||||||
|
// already have normalized values. We'll just multiply them
|
||||||
|
// by the stream dimensions to get real X and Y values rather
|
||||||
|
// than the client window dimensions.
|
||||||
|
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
|
||||||
|
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a drag timer when primary or secondary
|
||||||
|
// fingers go down
|
||||||
|
if (event->type == SDL_FINGERDOWN &&
|
||||||
|
(fingerIndex == 0 || fingerIndex == 1)) {
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
|
||||||
|
dragTimerCallback,
|
||||||
|
this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERMOTION) {
|
||||||
|
// If it's outside the deadzone delta, cancel drags and taps
|
||||||
|
if (qSqrt(qPow(event->x - m_TouchDownEvent[fingerIndex].x, 2) +
|
||||||
|
qPow(event->y - m_TouchDownEvent[fingerIndex].y, 2)) > DEAD_ZONE_DELTA) {
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = 0;
|
||||||
|
|
||||||
|
// This effectively cancels the tap logic below
|
||||||
|
m_TouchDownEvent[fingerIndex].timestamp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERUP) {
|
||||||
|
// Cancel the drag timer on finger up
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = 0;
|
||||||
|
|
||||||
|
// Release any drag
|
||||||
|
if (m_DragButton != 0) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
|
||||||
|
m_DragButton = 0;
|
||||||
|
}
|
||||||
|
// 2 finger tap
|
||||||
|
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
|
||||||
|
// Zero timestamp of the primary finger to ensure we won't
|
||||||
|
// generate a left click if the primary finger comes up soon.
|
||||||
|
m_TouchDownEvent[0].timestamp = 0;
|
||||||
|
|
||||||
|
// Press down the right mouse button
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
|
||||||
|
// Queue a timer to release it in 100 ms
|
||||||
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
|
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||||
|
releaseRightButtonTimerCallback,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
// 1 finger tap
|
||||||
|
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
|
||||||
|
// Press down the left mouse button
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
|
||||||
|
// Queue a timer to release it in 100 ms
|
||||||
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
|
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||||
|
releaseLeftButtonTimerCallback,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERDOWN) {
|
||||||
|
m_TouchDownEvent[fingerIndex] = *event;
|
||||||
|
}
|
||||||
|
else if (event->type == SDL_FINGERUP) {
|
||||||
|
m_TouchDownEvent[fingerIndex] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
-95
@@ -33,6 +33,16 @@
|
|||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
|
|
||||||
|
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||||
|
|
||||||
|
// Running the connection process asynchronously seems to reliably
|
||||||
|
// cause a crash in QSGRenderThread on Wayland and strange crashes
|
||||||
|
// elsewhere. Until these are figured out, avoid the async connect
|
||||||
|
// thread on Linux and BSDs.
|
||||||
|
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
|
||||||
|
#define USE_ASYNC_CONNECT_THREAD 1
|
||||||
|
#endif
|
||||||
|
|
||||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||||
Session::clStageStarting,
|
Session::clStageStarting,
|
||||||
nullptr,
|
nullptr,
|
||||||
@@ -53,24 +63,49 @@ void Session::clStageStarting(int stage)
|
|||||||
// which happens to be the main thread, so it's cool to interact
|
// which happens to be the main thread, so it's cool to interact
|
||||||
// with the GUI in these callbacks.
|
// with the GUI in these callbacks.
|
||||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
||||||
|
|
||||||
|
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::clStageFailed(int stage, int errorCode)
|
void Session::clStageFailed(int stage, int errorCode)
|
||||||
{
|
{
|
||||||
// We know this is called on the same thread as LiStartConnection()
|
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
|
||||||
// which happens to be the main thread, so it's cool to interact
|
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromStage(stage));
|
||||||
// with the GUI in these callbacks.
|
|
||||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
|
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
|
||||||
|
|
||||||
|
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::clConnectionTerminated(int errorCode)
|
void Session::clConnectionTerminated(int errorCode)
|
||||||
{
|
{
|
||||||
|
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromTerminationErrorCode(errorCode));
|
||||||
|
|
||||||
// Display the termination dialog if this was not intended
|
// Display the termination dialog if this was not intended
|
||||||
if (errorCode != 0) {
|
switch (errorCode) {
|
||||||
|
case ML_ERROR_GRACEFUL_TERMINATION:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||||
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
|
emit s_ActiveSession->displayLaunchError("No video received from host. Check the host PC's firewall and port forwarding rules.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ML_ERROR_NO_VIDEO_FRAME:
|
||||||
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
|
emit s_ActiveSession->displayLaunchError("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
s_ActiveSession->m_UnexpectedTermination = true;
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -326,6 +361,8 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
|||||||
m_InputHandler(nullptr),
|
m_InputHandler(nullptr),
|
||||||
m_InputHandlerLock(0),
|
m_InputHandlerLock(0),
|
||||||
m_MouseEmulationRefCount(0),
|
m_MouseEmulationRefCount(0),
|
||||||
|
m_AsyncConnectionSuccess(false),
|
||||||
|
m_PortTestResults(0),
|
||||||
m_OpusDecoder(nullptr),
|
m_OpusDecoder(nullptr),
|
||||||
m_AudioRenderer(nullptr),
|
m_AudioRenderer(nullptr),
|
||||||
m_AudioSampleCount(0),
|
m_AudioSampleCount(0),
|
||||||
@@ -353,7 +390,14 @@ bool Session::initialize()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a hidden window to use for decoder initialization tests
|
// Create a hidden window to use for decoder initialization tests
|
||||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||||
|
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create test window with platform flags: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
if (!testWindow) {
|
if (!testWindow) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to create window for hardware decode test: %s",
|
"Failed to create window for hardware decode test: %s",
|
||||||
@@ -361,6 +405,7 @@ bool Session::initialize()
|
|||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||||
@@ -510,6 +555,7 @@ void Session::emitLaunchWarning(QString text)
|
|||||||
// Pump the UI loop while we wait
|
// Pump the UI loop while we wait
|
||||||
SDL_Delay(5);
|
SDL_Delay(5);
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +742,7 @@ private:
|
|||||||
emit m_Session->quitStarting();
|
emit m_Session->quitStarting();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
emit m_Session->sessionFinished();
|
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish cleanup of the connection state
|
// Finish cleanup of the connection state
|
||||||
@@ -714,7 +760,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Session is finished now
|
// Session is finished now
|
||||||
emit m_Session->sessionFinished();
|
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,8 +913,8 @@ void Session::toggleFullscreen()
|
|||||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||||
|
|
||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN) {
|
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
|
||||||
// Confine the cursor to the window
|
// Confine the cursor to the window if we're capturing input
|
||||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,39 +949,37 @@ void Session::notifyMouseEmulationMode(bool enabled)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::exec(int displayOriginX, int displayOriginY)
|
class AsyncConnectionStartThread : public QThread
|
||||||
{
|
{
|
||||||
m_DisplayOriginX = displayOriginX;
|
public:
|
||||||
m_DisplayOriginY = displayOriginY;
|
AsyncConnectionStartThread(Session* session) :
|
||||||
|
QThread(nullptr),
|
||||||
|
m_Session(session) {}
|
||||||
|
|
||||||
// Complete initialization in this deferred context to avoid
|
void run() override
|
||||||
// calling expensive functions in the constructor (during the
|
{
|
||||||
// process of loading the StreamSegue).
|
m_Session->m_AsyncConnectionSuccess = m_Session->startConnectionAsync();
|
||||||
if (!initialize()) {
|
|
||||||
emit sessionFinished();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session* m_Session;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Called in a non-main thread
|
||||||
|
bool Session::startConnectionAsync()
|
||||||
|
{
|
||||||
// Wait 1.5 seconds before connecting to let the user
|
// Wait 1.5 seconds before connecting to let the user
|
||||||
// have time to read any messages present on the segue
|
// have time to read any messages present on the segue
|
||||||
|
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||||
|
SDL_Delay(1500);
|
||||||
|
#else
|
||||||
uint32_t start = SDL_GetTicks();
|
uint32_t start = SDL_GetTicks();
|
||||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||||
// Pump the UI loop while we wait
|
// Pump the UI loop while we wait since we're not async
|
||||||
SDL_Delay(5);
|
SDL_Delay(5);
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
// Wait for any old session to finish cleanup
|
|
||||||
s_ActiveSessionSemaphore.acquire();
|
|
||||||
|
|
||||||
// We're now active
|
|
||||||
s_ActiveSession = this;
|
|
||||||
|
|
||||||
// Initialize the gamepad code with our preferences
|
|
||||||
StreamingPreferences prefs;
|
|
||||||
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
|
|
||||||
m_StreamConfig.width,
|
|
||||||
m_StreamConfig.height);
|
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
@@ -952,23 +996,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Found host supported resolution: %dx%d",
|
"Found host supported resolution: %dx%d",
|
||||||
mode.width, mode.height);
|
mode.width, mode.height);
|
||||||
enableGameOptimizations = prefs.gameOptimizations;
|
enableGameOptimizations = m_Preferences->gameOptimizations;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prefs.fps > 60) {
|
|
||||||
// Using SOPS with FPS values over 60 causes GFE to fall back
|
|
||||||
// to 720p60. On previous GFE versions, we could avoid this by
|
|
||||||
// forcing the FPS value to 60 when launching the stream, but
|
|
||||||
// now on GFE 3.20.3 that seems to trigger some sort of
|
|
||||||
// frame rate limiter that locks the game to 60 FPS.
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Disabling SOPS for %d FPS stream",
|
|
||||||
prefs.fps);
|
|
||||||
enableGameOptimizations = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
@@ -977,23 +1009,15 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
else {
|
else {
|
||||||
http.launchApp(m_App.id, &m_StreamConfig,
|
http.launchApp(m_App.id, &m_StreamConfig,
|
||||||
enableGameOptimizations,
|
enableGameOptimizations,
|
||||||
prefs.playAudioOnHost,
|
m_Preferences->playAudioOnHost,
|
||||||
m_InputHandler->getAttachedGamepadMask());
|
m_InputHandler->getAttachedGamepadMask());
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
delete m_InputHandler;
|
|
||||||
m_InputHandler = nullptr;
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
return false;
|
||||||
return;
|
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
delete m_InputHandler;
|
|
||||||
m_InputHandler = nullptr;
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
emit displayLaunchError(e.toQString());
|
emit displayLaunchError(e.toQString());
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
||||||
@@ -1043,6 +1067,60 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
// listener.
|
// listener.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit connectionStarted();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::exec(int displayOriginX, int displayOriginY)
|
||||||
|
{
|
||||||
|
m_DisplayOriginX = displayOriginX;
|
||||||
|
m_DisplayOriginY = displayOriginY;
|
||||||
|
|
||||||
|
// Complete initialization in this deferred context to avoid
|
||||||
|
// calling expensive functions in the constructor (during the
|
||||||
|
// process of loading the StreamSegue).
|
||||||
|
//
|
||||||
|
// NB: This initializes the SDL video subsystem, so it must be
|
||||||
|
// called on the main thread.
|
||||||
|
if (!initialize()) {
|
||||||
|
emit sessionFinished(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for any old session to finish cleanup
|
||||||
|
s_ActiveSessionSemaphore.acquire();
|
||||||
|
|
||||||
|
// We're now active
|
||||||
|
s_ActiveSession = this;
|
||||||
|
|
||||||
|
// Initialize the gamepad code with our preferences
|
||||||
|
// NB: m_InputHandler must be initialize before starting the connection.
|
||||||
|
m_InputHandler = new SdlInputHandler(*m_Preferences, m_Computer,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height);
|
||||||
|
|
||||||
|
// Kick off the async connection thread while we sit here and pump the event loop
|
||||||
|
AsyncConnectionStartThread asyncConnThread(this);
|
||||||
|
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||||
|
asyncConnThread.start();
|
||||||
|
while (!asyncConnThread.wait(10)) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
asyncConnThread.run();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Pump the event loop one last time to ensure we pick up any events from
|
||||||
|
// the thread that happened while it was in the final successful QThread::wait().
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
|
||||||
|
// If the connection failed, clean up and abort the connection.
|
||||||
|
if (!m_AsyncConnectionSuccess) {
|
||||||
delete m_InputHandler;
|
delete m_InputHandler;
|
||||||
m_InputHandler = nullptr;
|
m_InputHandler = nullptr;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
@@ -1050,10 +1128,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pump the message loop to update the UI
|
|
||||||
emit connectionStarted();
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
|
|
||||||
int x, y, width, height;
|
int x, y, width, height;
|
||||||
getWindowDimensions(x, y, width, height);
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
@@ -1064,6 +1138,17 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_Delay(500);
|
SDL_Delay(500);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
m_Window = SDL_CreateWindow("Moonlight",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils::getPlatformWindowFlags());
|
||||||
|
if (!m_Window) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_CreateWindow() failed with platform flags: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
m_Window = SDL_CreateWindow("Moonlight",
|
m_Window = SDL_CreateWindow("Moonlight",
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
@@ -1074,12 +1159,16 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
|
||||||
delete m_InputHandler;
|
delete m_InputHandler;
|
||||||
m_InputHandler = nullptr;
|
m_InputHandler = nullptr;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_InputHandler->setWindow(m_Window);
|
||||||
|
|
||||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||||
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||||
@@ -1118,11 +1207,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN) {
|
|
||||||
// Confine the cursor to the window
|
|
||||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the window display mode based on our current monitor
|
// Update the window display mode based on our current monitor
|
||||||
updateOptimalWindowDisplayMode();
|
updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
@@ -1131,33 +1215,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool needsFirstEnterCapture = false;
|
bool needsFirstEnterCapture = false;
|
||||||
|
bool needsPostDecoderCreationCapture = false;
|
||||||
|
|
||||||
// Capture the mouse in relative mode by default on release builds only.
|
|
||||||
// This prevents the mouse from becoming trapped inside Moonlight when
|
|
||||||
// it's halted at a debug break.
|
|
||||||
#ifdef QT_DEBUG
|
|
||||||
if (prefs.absoluteMouseMode)
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
|
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
|
||||||
// event where it seems to work consistently on GNOME. This doesn't work for
|
// event where it seems to work consistently on GNOME. For other platforms,
|
||||||
// XWayland though.
|
// especially where SDL may call SDL_RecreateWindow(), we must only capture
|
||||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
|
// after the decoder is created.
|
||||||
// We know we aren't running on native Wayland now, but
|
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||||
// we still may be running on XWayland.
|
|
||||||
if (!WMUtils::isRunningWayland()) {
|
|
||||||
// Neither Wayland or XWayland: capture now
|
|
||||||
m_InputHandler->setCaptureActive(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// XWayland: mouse capture doesn't work reliably, so let the user
|
|
||||||
// engage the mouse capture via clicking or using the hotkey.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
|
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
|
||||||
needsFirstEnterCapture = true;
|
needsFirstEnterCapture = true;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// X11/XWayland: Capture after decoder creation
|
||||||
|
needsPostDecoderCreationCapture = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop text input. SDL enables it by default
|
// Stop text input. SDL enables it by default
|
||||||
@@ -1186,7 +1256,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_UnexpectedTermination = false;
|
m_UnexpectedTermination = false;
|
||||||
|
|
||||||
// Start rich presence to indicate we're in game
|
// Start rich presence to indicate we're in game
|
||||||
RichPresenceManager presence(prefs, m_App.name);
|
RichPresenceManager presence(*m_Preferences, m_App.name);
|
||||||
|
|
||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// 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.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
@@ -1214,16 +1284,27 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
goto DispatchDeferredCleanup;
|
goto DispatchDeferredCleanup;
|
||||||
|
|
||||||
case SDL_USEREVENT:
|
case SDL_USEREVENT:
|
||||||
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
|
switch (event.user.code) {
|
||||||
|
case SDL_CODE_FRAME_READY:
|
||||||
m_VideoDecoder->renderFrameOnMainThread();
|
m_VideoDecoder->renderFrameOnMainThread();
|
||||||
break;
|
break;
|
||||||
|
case SDL_CODE_HIDE_CURSOR:
|
||||||
|
SDL_ShowCursor(SDL_DISABLE);
|
||||||
|
break;
|
||||||
|
case SDL_CODE_SHOW_CURSOR:
|
||||||
|
SDL_ShowCursor(SDL_ENABLE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
|
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||||
m_InputHandler->notifyFocusGained(m_Window);
|
m_InputHandler->notifyFocusLost();
|
||||||
}
|
}
|
||||||
else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
else if (event.window.event == SDL_WINDOWEVENT_LEAVE) {
|
||||||
m_InputHandler->notifyFocusLost(m_Window);
|
m_InputHandler->notifyMouseLeave();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
|
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
|
||||||
@@ -1258,6 +1339,12 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_SetWindowPosition(m_Window, x, y);
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Recreating renderer for window event: %d (%d %d)",
|
||||||
|
event.window.event,
|
||||||
|
event.window.data1,
|
||||||
|
event.window.data2);
|
||||||
|
|
||||||
// Fall through
|
// Fall through
|
||||||
case SDL_RENDER_DEVICE_RESET:
|
case SDL_RENDER_DEVICE_RESET:
|
||||||
case SDL_RENDER_TARGETS_RESET:
|
case SDL_RENDER_TARGETS_RESET:
|
||||||
@@ -1310,6 +1397,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||||
goto DispatchDeferredCleanup;
|
goto DispatchDeferredCleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As of SDL 2.0.12, SDL_RecreateWindow() doesn't carry over mouse capture
|
||||||
|
// or mouse hiding state to the new window. By capturing after the decoder
|
||||||
|
// is set up, this ensures the window re-creation is already done.
|
||||||
|
if (needsPostDecoderCreationCapture) {
|
||||||
|
m_InputHandler->setCaptureActive(true);
|
||||||
|
needsFirstEnterCapture = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request an IDR frame to complete the reset
|
// Request an IDR frame to complete the reset
|
||||||
@@ -1327,7 +1422,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_InputHandler->handleMouseButtonEvent(&event.button);
|
m_InputHandler->handleMouseButtonEvent(&event.button);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEMOTION:
|
case SDL_MOUSEMOTION:
|
||||||
m_InputHandler->handleMouseMotionEvent(m_Window, &event.motion);
|
m_InputHandler->handleMouseMotionEvent(&event.motion);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEWHEEL:
|
case SDL_MOUSEWHEEL:
|
||||||
m_InputHandler->handleMouseWheelEvent(&event.wheel);
|
m_InputHandler->handleMouseWheelEvent(&event.wheel);
|
||||||
@@ -1349,7 +1444,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
case SDL_FINGERDOWN:
|
case SDL_FINGERDOWN:
|
||||||
case SDL_FINGERMOTION:
|
case SDL_FINGERMOTION:
|
||||||
case SDL_FINGERUP:
|
case SDL_FINGERUP:
|
||||||
m_InputHandler->handleTouchFingerEvent(m_Window, &event.tfinger);
|
m_InputHandler->handleTouchFingerEvent(&event.tfinger);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1358,7 +1453,6 @@ DispatchDeferredCleanup:
|
|||||||
// Uncapture the mouse and hide the window immediately,
|
// Uncapture the mouse and hide the window immediately,
|
||||||
// so we can return to the Qt GUI ASAP.
|
// so we can return to the Qt GUI ASAP.
|
||||||
m_InputHandler->setCaptureActive(false);
|
m_InputHandler->setCaptureActive(false);
|
||||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
if (QGuiApplication::platformName() == "eglfs") {
|
if (QGuiApplication::platformName() == "eglfs") {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <opus_multistream.h>
|
#include <opus_multistream.h>
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "input.h"
|
#include "input/input.h"
|
||||||
#include "video/decoder.h"
|
#include "video/decoder.h"
|
||||||
#include "audio/renderers/renderer.h"
|
#include "audio/renderers/renderer.h"
|
||||||
#include "video/overlaymanager.h"
|
#include "video/overlaymanager.h"
|
||||||
@@ -16,6 +16,7 @@ class Session : public QObject
|
|||||||
|
|
||||||
friend class SdlInputHandler;
|
friend class SdlInputHandler;
|
||||||
friend class DeferredSessionCleanupTask;
|
friend class DeferredSessionCleanupTask;
|
||||||
|
friend class AsyncConnectionStartThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||||
@@ -51,11 +52,13 @@ signals:
|
|||||||
|
|
||||||
void quitStarting();
|
void quitStarting();
|
||||||
|
|
||||||
void sessionFinished();
|
void sessionFinished(int portTestResult);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initialize();
|
bool initialize();
|
||||||
|
|
||||||
|
bool startConnectionAsync();
|
||||||
|
|
||||||
bool validateLaunch(SDL_Window* testWindow);
|
bool validateLaunch(SDL_Window* testWindow);
|
||||||
|
|
||||||
void emitLaunchWarning(QString text);
|
void emitLaunchWarning(QString text);
|
||||||
@@ -147,6 +150,9 @@ private:
|
|||||||
SDL_SpinLock m_InputHandlerLock;
|
SDL_SpinLock m_InputHandlerLock;
|
||||||
int m_MouseEmulationRefCount;
|
int m_MouseEmulationRefCount;
|
||||||
|
|
||||||
|
bool m_AsyncConnectionSuccess;
|
||||||
|
int m_PortTestResults;
|
||||||
|
|
||||||
int m_ActiveVideoFormat;
|
int m_ActiveVideoFormat;
|
||||||
int m_ActiveVideoWidth;
|
int m_ActiveVideoWidth;
|
||||||
int m_ActiveVideoHeight;
|
int m_ActiveVideoHeight;
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Uint32 StreamUtils::getPlatformWindowFlags()
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
return SDL_WINDOW_METAL;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||||
{
|
{
|
||||||
int dstH = dst->w * src->h / src->w;
|
int dstH = dst->w * src->h / src->w;
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
class StreamUtils
|
class StreamUtils
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
static
|
||||||
|
Uint32 getPlatformWindowFlags();
|
||||||
|
|
||||||
static
|
static
|
||||||
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ int DrmRenderer::getRendererAttributes()
|
|||||||
|
|
||||||
void DrmRenderer::renderFrame(AVFrame* frame)
|
void DrmRenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||||
int err;
|
int err;
|
||||||
uint32_t primeHandle;
|
uint32_t primeHandle;
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
|||||||
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
|
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
|
||||||
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
|
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Intel driver version blacklisted for HEVC");
|
"Detected buggy Intel GPU driver installed. Update your Intel GPU driver to enable HEVC!");
|
||||||
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -812,6 +812,11 @@ int DXVA2Renderer::getDecoderColorspace()
|
|||||||
|
|
||||||
void DXVA2Renderer::renderFrame(AVFrame *frame)
|
void DXVA2Renderer::renderFrame(AVFrame *frame)
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <SDL_egl.h>
|
||||||
|
|
||||||
|
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
||||||
|
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
||||||
|
if (!EGLExtensionsStr) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
|
||||||
|
return QStringList();
|
||||||
|
}
|
||||||
|
return QString(EGLExtensionsStr).split(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLExtensions::EGLExtensions(EGLDisplay dpy) :
|
||||||
|
m_Extensions(egl_get_extensions(dpy))
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool EGLExtensions::isSupported(const QString &extension) const {
|
||||||
|
return m_Extensions.contains(extension);
|
||||||
|
}
|
||||||
@@ -0,0 +1,633 @@
|
|||||||
|
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
|
||||||
|
#include "eglvid.h"
|
||||||
|
|
||||||
|
#include "path.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <SDL_egl.h>
|
||||||
|
#include <SDL_opengles2.h>
|
||||||
|
#include <SDL_render.h>
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#ifndef EGL_VERSION_1_5
|
||||||
|
typedef intptr_t EGLAttrib;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||||
|
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef EGL_EXT_platform_base
|
||||||
|
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// These are EGL extensions, so some platform headers may not provide them
|
||||||
|
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
||||||
|
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
||||||
|
#endif
|
||||||
|
#ifndef EGL_PLATFORM_X11_KHR
|
||||||
|
#define EGL_PLATFORM_X11_KHR 0x31D5
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* TODO:
|
||||||
|
* - handle more pixel formats
|
||||||
|
* - handle software decoding
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* DOC/misc:
|
||||||
|
* - https://kernel-recipes.org/en/2016/talks/video-and-colorspaces/
|
||||||
|
* - http://www.brucelindbloom.com/
|
||||||
|
* - https://learnopengl.com/Getting-started/Shaders
|
||||||
|
* - https://github.com/stunpix/yuvit
|
||||||
|
* - https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
|
||||||
|
* - https://www.renesas.com/eu/en/www/doc/application-note/an9717.pdf
|
||||||
|
* - https://www.xilinx.com/support/documentation/application_notes/xapp283.pdf
|
||||||
|
* - https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
|
||||||
|
* - https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
|
||||||
|
* - https://gist.github.com/rexguo/6696123
|
||||||
|
* - https://wiki.libsdl.org/CategoryVideo
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define EGL_LOG(Category, ...) SDL_Log ## Category(\
|
||||||
|
SDL_LOG_CATEGORY_APPLICATION, \
|
||||||
|
"EGLRenderer: " __VA_ARGS__)
|
||||||
|
|
||||||
|
SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
|
||||||
|
|
||||||
|
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||||
|
:
|
||||||
|
m_SwPixelFormat(AV_PIX_FMT_NONE),
|
||||||
|
m_EGLDisplay(EGL_NO_DISPLAY),
|
||||||
|
m_Textures{0},
|
||||||
|
m_ShaderProgram(0),
|
||||||
|
m_Context(0),
|
||||||
|
m_Window(nullptr),
|
||||||
|
m_Backend(backendRenderer),
|
||||||
|
m_VAO(0),
|
||||||
|
m_ColorSpace(AVCOL_SPC_NB),
|
||||||
|
m_ColorFull(false),
|
||||||
|
m_BlockingSwapBuffers(false),
|
||||||
|
m_glEGLImageTargetTexture2DOES(nullptr),
|
||||||
|
m_glGenVertexArraysOES(nullptr),
|
||||||
|
m_glBindVertexArrayOES(nullptr),
|
||||||
|
m_glDeleteVertexArraysOES(nullptr),
|
||||||
|
m_DummyRenderer(nullptr)
|
||||||
|
{
|
||||||
|
SDL_assert(backendRenderer);
|
||||||
|
SDL_assert(backendRenderer->canExportEGL());
|
||||||
|
|
||||||
|
// Save these global parameters so we can restore them in our destructor
|
||||||
|
SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &m_OldContextProfileMask);
|
||||||
|
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &m_OldContextMajorVersion);
|
||||||
|
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &m_OldContextMinorVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
EGLRenderer::~EGLRenderer()
|
||||||
|
{
|
||||||
|
if (m_Context) {
|
||||||
|
// Reattach the GL context to the main thread for destruction
|
||||||
|
SDL_GL_MakeCurrent(m_Window, m_Context);
|
||||||
|
if (m_ShaderProgram) {
|
||||||
|
glDeleteProgram(m_ShaderProgram);
|
||||||
|
}
|
||||||
|
if (m_VAO) {
|
||||||
|
SDL_assert(m_glDeleteVertexArraysOES != nullptr);
|
||||||
|
m_glDeleteVertexArraysOES(1, &m_VAO);
|
||||||
|
}
|
||||||
|
SDL_GL_DeleteContext(m_Context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_DummyRenderer) {
|
||||||
|
SDL_DestroyRenderer(m_DummyRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the global properties back to what they were before
|
||||||
|
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, m_OldContextMajorVersion);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, m_OldContextMinorVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||||
|
{
|
||||||
|
/* Nothing to do */
|
||||||
|
|
||||||
|
EGL_LOG(Info, "Using EGL renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType)
|
||||||
|
{
|
||||||
|
// TODO: FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
|
||||||
|
{
|
||||||
|
// Remember to keep this in sync with EGLRenderer::renderFrame()!
|
||||||
|
switch (pixelFormat)
|
||||||
|
{
|
||||||
|
case AV_PIX_FMT_NV12:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int EGLRenderer::loadAndBuildShader(int shaderType,
|
||||||
|
const char *file) {
|
||||||
|
GLuint shader = glCreateShader(shaderType);
|
||||||
|
if (!shader || shader == GL_INVALID_ENUM) {
|
||||||
|
EGL_LOG(Error, "Can't create shader: %d", glGetError());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sourceData = Path::readDataFile(file);
|
||||||
|
GLint len = sourceData.size();
|
||||||
|
const char *buf = sourceData.data();
|
||||||
|
|
||||||
|
glShaderSource(shader, 1, &buf, &len);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint status;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
if (!status) {
|
||||||
|
char shaderLog[512];
|
||||||
|
glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog);
|
||||||
|
EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::openDisplay(unsigned int platform, void* nativeDisplay)
|
||||||
|
{
|
||||||
|
PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplayProc;
|
||||||
|
PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTProc;
|
||||||
|
|
||||||
|
m_EGLDisplay = EGL_NO_DISPLAY;
|
||||||
|
|
||||||
|
// NB: eglGetPlatformDisplay() and eglGetPlatformDisplayEXT() have slightly different definitions
|
||||||
|
eglGetPlatformDisplayProc = (typeof(eglGetPlatformDisplayProc))eglGetProcAddress("eglGetPlatformDisplay");
|
||||||
|
eglGetPlatformDisplayEXTProc = (typeof(eglGetPlatformDisplayEXTProc))eglGetProcAddress("eglGetPlatformDisplayEXT");
|
||||||
|
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
// eglGetPlatformDisplay() is part of the EGL 1.5 core specification
|
||||||
|
if (eglGetPlatformDisplayProc != nullptr) {
|
||||||
|
m_EGLDisplay = eglGetPlatformDisplayProc(platform, nativeDisplay, nullptr);
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
EGL_LOG(Warn, "eglGetPlatformDisplay() failed: %d", eglGetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
// eglGetPlatformDisplayEXT() is an extension for EGL 1.4
|
||||||
|
const EGLExtensions eglExtensions(EGL_NO_DISPLAY);
|
||||||
|
if (eglExtensions.isSupported("EGL_EXT_platform_base")) {
|
||||||
|
if (eglGetPlatformDisplayEXTProc != nullptr) {
|
||||||
|
m_EGLDisplay = eglGetPlatformDisplayEXTProc(platform, nativeDisplay, nullptr);
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
EGL_LOG(Warn, "eglGetPlatformDisplayEXT() failed: %d", eglGetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
EGL_LOG(Warn, "EGL_EXT_platform_base supported but no eglGetPlatformDisplayEXT() export!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
// Finally, if all else fails, use eglGetDisplay()
|
||||||
|
m_EGLDisplay = eglGetDisplay((EGLNativeDisplayType)nativeDisplay);
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
EGL_LOG(Error, "eglGetDisplay() failed: %d", eglGetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_EGLDisplay != EGL_NO_DISPLAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::compileShader() {
|
||||||
|
SDL_assert(!m_ShaderProgram);
|
||||||
|
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||||
|
|
||||||
|
// XXX: TODO: other formats
|
||||||
|
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||||
|
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
|
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, "egl.vert");
|
||||||
|
if (!vertexShader)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, "egl.frag");
|
||||||
|
if (!fragmentShader)
|
||||||
|
goto fragError;
|
||||||
|
|
||||||
|
m_ShaderProgram = glCreateProgram();
|
||||||
|
if (!m_ShaderProgram) {
|
||||||
|
EGL_LOG(Error, "Cannot create shader program");
|
||||||
|
goto progFailCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
glAttachShader(m_ShaderProgram, vertexShader);
|
||||||
|
glAttachShader(m_ShaderProgram, fragmentShader);
|
||||||
|
glLinkProgram(m_ShaderProgram);
|
||||||
|
int status;
|
||||||
|
glGetProgramiv(m_ShaderProgram, GL_LINK_STATUS, &status);
|
||||||
|
if (status) {
|
||||||
|
ret = true;
|
||||||
|
} else {
|
||||||
|
char shader_log[512];
|
||||||
|
glGetProgramInfoLog(m_ShaderProgram, sizeof (shader_log), nullptr, shader_log);
|
||||||
|
EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
|
||||||
|
glDeleteProgram(m_ShaderProgram);
|
||||||
|
m_ShaderProgram = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
progFailCreate:
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
fragError:
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||||
|
{
|
||||||
|
m_Window = params->window;
|
||||||
|
|
||||||
|
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||||
|
// EGL doesn't support rendering YUV 10-bit textures yet
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's not safe to attempt to opportunistically create a GLES2
|
||||||
|
// renderer prior to 2.0.10. If GLES2 isn't available, SDL will
|
||||||
|
// attempt to dereference a null pointer and crash Moonlight.
|
||||||
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=4350
|
||||||
|
// https://hg.libsdl.org/SDL/rev/84618d571795
|
||||||
|
if (!SDL_VERSION_ATLEAST(2, 0, 10)) {
|
||||||
|
EGL_LOG(Error, "Not supported until SDL 2.0.10");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: Work around bug where renderer will repeatedly fail with:
|
||||||
|
// SDL_CreateRenderer() failed: Could not create GLES window surface
|
||||||
|
if (m_Window == s_LastFailedWindow) {
|
||||||
|
EGL_LOG(Error, "SDL_CreateRenderer() already failed on this window!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create a dummy renderer in order to craft an accelerated SDL Window.
|
||||||
|
* Request opengl ES 3.0 context, otherwise it will SIGSEGV
|
||||||
|
* https://gitlab.freedesktop.org/mesa/mesa/issues/1011
|
||||||
|
*/
|
||||||
|
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||||
|
|
||||||
|
int renderIndex;
|
||||||
|
int maxRenderers = SDL_GetNumRenderDrivers();
|
||||||
|
SDL_assert(maxRenderers >= 0);
|
||||||
|
|
||||||
|
SDL_RendererInfo renderInfo;
|
||||||
|
for (renderIndex = 0; renderIndex < maxRenderers; ++renderIndex) {
|
||||||
|
if (SDL_GetRenderDriverInfo(renderIndex, &renderInfo))
|
||||||
|
continue;
|
||||||
|
if (!strcmp(renderInfo.name, "opengles2")) {
|
||||||
|
SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (renderIndex == maxRenderers) {
|
||||||
|
EGL_LOG(Error, "Could not find a suitable SDL_Renderer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED))) {
|
||||||
|
EGL_LOG(Error, "SDL_CreateRenderer() failed: %s", SDL_GetError());
|
||||||
|
s_LastFailedWindow = m_Window;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||||
|
EGL_LOG(Error, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (info.subsystem) {
|
||||||
|
#ifdef SDL_VIDEO_DRIVER_WAYLAND
|
||||||
|
case SDL_SYSWM_WAYLAND:
|
||||||
|
if (!openDisplay(EGL_PLATFORM_WAYLAND_KHR, info.info.wl.display)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
#ifdef SDL_VIDEO_DRIVER_X11
|
||||||
|
case SDL_SYSWM_X11:
|
||||||
|
if (!openDisplay(EGL_PLATFORM_X11_KHR, info.info.x11.display)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
default:
|
||||||
|
EGL_LOG(Error, "not compatible with SYSWM");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||||
|
EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(m_Context = SDL_GL_CreateContext(params->window))) {
|
||||||
|
EGL_LOG(Error, "Cannot create OpenGL context: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (SDL_GL_MakeCurrent(params->window, m_Context)) {
|
||||||
|
EGL_LOG(Error, "Cannot use created EGL context: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EGLExtensions eglExtensions(m_EGLDisplay);
|
||||||
|
if (!eglExtensions.isSupported("EGL_KHR_image_base") &&
|
||||||
|
!eglExtensions.isSupported("EGL_KHR_image")) {
|
||||||
|
EGL_LOG(Error, "EGL_KHR_image unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!SDL_GL_ExtensionSupported("GL_OES_EGL_image")) {
|
||||||
|
EGL_LOG(Error, "GL_OES_EGL_image unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Backend->initializeEGL(m_EGLDisplay, eglExtensions))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!(m_glEGLImageTargetTexture2DOES = (typeof(m_glEGLImageTargetTexture2DOES))eglGetProcAddress("glEGLImageTargetTexture2DOES"))) {
|
||||||
|
EGL_LOG(Error,
|
||||||
|
"EGL: cannot retrieve `glEGLImageTargetTexture2DOES` address");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex arrays are an extension on OpenGL ES 2.0
|
||||||
|
if (SDL_GL_ExtensionSupported("GL_OES_vertex_array_object")) {
|
||||||
|
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArraysOES");
|
||||||
|
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArrayOES");
|
||||||
|
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArraysOES");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// They are included in OpenGL ES 3.0 as part of the standard
|
||||||
|
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArrays");
|
||||||
|
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArray");
|
||||||
|
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArrays");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_glGenVertexArraysOES || !m_glBindVertexArrayOES || !m_glDeleteVertexArraysOES) {
|
||||||
|
EGL_LOG(Error, "Failed to find VAO functions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compute the video region size in order to keep the aspect ratio of the
|
||||||
|
* video stream.
|
||||||
|
*/
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = dst.x = dst.y = 0;
|
||||||
|
src.w = params->width;
|
||||||
|
src.h = params->height;
|
||||||
|
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||||
|
|
||||||
|
glClearColor(0, 0, 0, 1);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
if (params->enableVsync) {
|
||||||
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
m_BlockingSwapBuffers = true;
|
||||||
|
} else {
|
||||||
|
SDL_GL_SetSwapInterval(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GL_SwapWindow(params->window);
|
||||||
|
|
||||||
|
glGenTextures(EGL_MAX_PLANES, m_Textures);
|
||||||
|
for (size_t i = 0; i < EGL_MAX_PLANES; ++i) {
|
||||||
|
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLenum err = glGetError();
|
||||||
|
if (err != GL_NO_ERROR)
|
||||||
|
EGL_LOG(Error, "OpenGL error: %d", err);
|
||||||
|
|
||||||
|
// Detach the context from this thread, so the render thread can attach it
|
||||||
|
SDL_GL_MakeCurrent(m_Window, nullptr);
|
||||||
|
|
||||||
|
return err == GL_NO_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float *EGLRenderer::getColorMatrix() {
|
||||||
|
/* The conversion matrices are shamelessly stolen from linux:
|
||||||
|
* drivers/media/platform/imx-pxp.c:pxp_setup_csc
|
||||||
|
*/
|
||||||
|
static const float bt601Lim[] = {
|
||||||
|
1.1644f, 1.1644f, 1.1644f,
|
||||||
|
0.0f, -0.3917f, 2.0172f,
|
||||||
|
1.5960f, -0.8129f, 0.0f
|
||||||
|
};
|
||||||
|
static const float bt601Full[] = {
|
||||||
|
1.0f, 1.0f, 1.0f,
|
||||||
|
0.0f, -0.3441f, 1.7720f,
|
||||||
|
1.4020f, -0.7141f, 0.0f
|
||||||
|
};
|
||||||
|
static const float bt709Lim[] = {
|
||||||
|
1.1644f, 1.1644f, 1.1644f,
|
||||||
|
0.0f, -0.2132f, 2.1124f,
|
||||||
|
1.7927f, -0.5329f, 0.0f
|
||||||
|
};
|
||||||
|
static const float bt709Full[] = {
|
||||||
|
1.0f, 1.0f, 1.0f,
|
||||||
|
0.0f, -0.1873f, 1.8556f,
|
||||||
|
1.5748f, -0.4681f, 0.0f
|
||||||
|
};
|
||||||
|
static const float bt2020Lim[] = {
|
||||||
|
1.1644f, 1.1644f, 1.1644f,
|
||||||
|
0.0f, -0.1874f, 2.1418f,
|
||||||
|
1.6781f, -0.6505f, 0.0f
|
||||||
|
};
|
||||||
|
static const float bt2020Full[] = {
|
||||||
|
1.0f, 1.0f, 1.0f,
|
||||||
|
0.0f, -0.1646f, 1.8814f,
|
||||||
|
1.4746f, -0.5714f, 0.0f
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (m_ColorSpace) {
|
||||||
|
case AVCOL_SPC_SMPTE170M:
|
||||||
|
case AVCOL_SPC_BT470BG:
|
||||||
|
EGL_LOG(Info, "BT-601 pixels");
|
||||||
|
return m_ColorFull ? bt601Full : bt601Lim;
|
||||||
|
case AVCOL_SPC_BT709:
|
||||||
|
EGL_LOG(Info, "BT-709 pixels");
|
||||||
|
return m_ColorFull ? bt709Full : bt709Lim;
|
||||||
|
case AVCOL_SPC_BT2020_NCL:
|
||||||
|
case AVCOL_SPC_BT2020_CL:
|
||||||
|
EGL_LOG(Info, "BT-2020 pixels");
|
||||||
|
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||||
|
};
|
||||||
|
EGL_LOG(Warn, "unknown color space: %d, falling back to BT-601",
|
||||||
|
m_ColorSpace);
|
||||||
|
return bt601Lim;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::specialize() {
|
||||||
|
SDL_assert(!m_VAO);
|
||||||
|
|
||||||
|
// Attach our GL context to the render thread
|
||||||
|
SDL_GL_MakeCurrent(m_Window, m_Context);
|
||||||
|
|
||||||
|
if (!compileShader())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// The viewport should have the aspect ratio of the video stream
|
||||||
|
static const float vertices[] = {
|
||||||
|
// pos .... // tex coords
|
||||||
|
1.0f, 1.0f, 1.0f, 0.0f,
|
||||||
|
1.0f, -1.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f,
|
||||||
|
|
||||||
|
};
|
||||||
|
static const unsigned int indices[] = {
|
||||||
|
0, 1, 3,
|
||||||
|
1, 2, 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
glUseProgram(m_ShaderProgram);
|
||||||
|
|
||||||
|
unsigned int VBO, EBO;
|
||||||
|
m_glGenVertexArraysOES(1, &m_VAO);
|
||||||
|
glGenBuffers(1, &VBO);
|
||||||
|
glGenBuffers(1, &EBO);
|
||||||
|
|
||||||
|
m_glBindVertexArrayOES(m_VAO);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof (float)));
|
||||||
|
glEnableVertexAttribArray(1);
|
||||||
|
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
m_glBindVertexArrayOES(0);
|
||||||
|
|
||||||
|
int yuvmatLocation = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||||
|
glUniformMatrix3fv(yuvmatLocation, 1, GL_FALSE, getColorMatrix());
|
||||||
|
|
||||||
|
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||||
|
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||||
|
|
||||||
|
int offLocation = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||||
|
glUniform3fv(offLocation, 1, m_ColorFull ? fullOffsets : limitedOffsets);
|
||||||
|
|
||||||
|
int colorPlane = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||||
|
glUniform1i(colorPlane, 0);
|
||||||
|
colorPlane = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||||
|
glUniform1i(colorPlane, 1);
|
||||||
|
|
||||||
|
glDeleteBuffers(1, &VBO);
|
||||||
|
glDeleteBuffers(1, &EBO);
|
||||||
|
|
||||||
|
GLenum err = glGetError();
|
||||||
|
if (err != GL_NO_ERROR) {
|
||||||
|
EGL_LOG(Error, "OpenGL error: %d", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return err == GL_NO_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EGLRenderer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
EGLImage imgs[EGL_MAX_PLANES];
|
||||||
|
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - unbind the GL context
|
||||||
|
SDL_GL_MakeCurrent(m_Window, nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->hw_frames_ctx != nullptr) {
|
||||||
|
// Find the native read-back format and load the shader
|
||||||
|
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
|
||||||
|
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||||
|
|
||||||
|
m_SwPixelFormat = hwFrameCtx->sw_format;
|
||||||
|
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||||
|
|
||||||
|
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
|
||||||
|
|
||||||
|
// XXX: TODO: Handle other pixel formats
|
||||||
|
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||||
|
m_ColorSpace = frame->colorspace;
|
||||||
|
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||||
|
|
||||||
|
if (!specialize()) {
|
||||||
|
m_SwPixelFormat = AV_PIX_FMT_NONE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||||
|
if (plane_count < 0)
|
||||||
|
return;
|
||||||
|
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||||
|
glActiveTexture(GL_TEXTURE0 + i);
|
||||||
|
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||||
|
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: load texture for SW decoding ?
|
||||||
|
EGL_LOG(Error, "EGL rendering only supports hw frames");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glUseProgram(m_ShaderProgram);
|
||||||
|
m_glBindVertexArrayOES(m_VAO);
|
||||||
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||||
|
|
||||||
|
SDL_GL_SwapWindow(m_Window);
|
||||||
|
|
||||||
|
if (m_BlockingSwapBuffers) {
|
||||||
|
// This glClear() forces us to block until the buffer swap is
|
||||||
|
// complete to continue rendering. Mesa won't actually wait
|
||||||
|
// for the swap with just glFinish() alone. Waiting here keeps us
|
||||||
|
// in lock step with the display refresh rate. If we don't wait
|
||||||
|
// here, we'll stall on the first GL call next frame. Doing the
|
||||||
|
// wait here instead allows more time for a newer frame to arrive
|
||||||
|
// for next renderFrame() call.
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame->hw_frames_ctx != nullptr)
|
||||||
|
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <SDL_opengles2.h>
|
||||||
|
#include <SDL_opengles2_gl2ext.h>
|
||||||
|
|
||||||
|
class EGLRenderer : public IFFmpegRenderer {
|
||||||
|
public:
|
||||||
|
EGLRenderer(IFFmpegRenderer *backendRenderer);
|
||||||
|
virtual ~EGLRenderer() override;
|
||||||
|
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||||
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||||
|
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
bool compileShader();
|
||||||
|
bool specialize();
|
||||||
|
const float *getColorMatrix();
|
||||||
|
static int loadAndBuildShader(int shaderType, const char *filename);
|
||||||
|
bool openDisplay(unsigned int platform, void* nativeDisplay);
|
||||||
|
|
||||||
|
int m_SwPixelFormat;
|
||||||
|
void *m_EGLDisplay;
|
||||||
|
unsigned m_Textures[EGL_MAX_PLANES];
|
||||||
|
unsigned m_ShaderProgram;
|
||||||
|
SDL_GLContext m_Context;
|
||||||
|
SDL_Window *m_Window;
|
||||||
|
IFFmpegRenderer *m_Backend;
|
||||||
|
unsigned int m_VAO;
|
||||||
|
int m_ColorSpace;
|
||||||
|
bool m_ColorFull;
|
||||||
|
bool m_BlockingSwapBuffers;
|
||||||
|
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
||||||
|
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
||||||
|
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
||||||
|
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
||||||
|
|
||||||
|
int m_OldContextProfileMask;
|
||||||
|
int m_OldContextMajorVersion;
|
||||||
|
int m_OldContextMinorVersion;
|
||||||
|
|
||||||
|
SDL_Renderer *m_DummyRenderer;
|
||||||
|
|
||||||
|
// HACK: Work around bug where renderer will repeatedly fail with:
|
||||||
|
// SDL_CreateRenderer() failed: Could not create GLES window surface
|
||||||
|
static SDL_Window* s_LastFailedWindow;
|
||||||
|
};
|
||||||
@@ -147,6 +147,11 @@ bool MmalRenderer::needsTestFrame()
|
|||||||
|
|
||||||
void MmalRenderer::renderFrame(AVFrame* frame)
|
void MmalRenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
||||||
MMAL_STATUS_T status;
|
MMAL_STATUS_T status;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ Pacer::~Pacer()
|
|||||||
m_RenderQueueNotEmpty.wakeAll();
|
m_RenderQueueNotEmpty.wakeAll();
|
||||||
SDL_WaitThread(m_RenderThread, nullptr);
|
SDL_WaitThread(m_RenderThread, nullptr);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Send a null AVFrame to indicate end of stream on the main thread
|
||||||
|
m_VsyncRenderer->renderFrame(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete any remaining unconsumed frames
|
// Delete any remaining unconsumed frames
|
||||||
while (!m_RenderQueue.isEmpty()) {
|
while (!m_RenderQueue.isEmpty()) {
|
||||||
@@ -106,6 +110,9 @@ int Pacer::renderThread(void* context)
|
|||||||
me->renderLastFrameAndUnlock();
|
me->renderLastFrameAndUnlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a null AVFrame to indicate end of stream on the render thread
|
||||||
|
me->m_VsyncRenderer->renderFrame(nullptr);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,23 @@ extern "C" {
|
|||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
// SDL_egl.h have too many conflicts, we will do without
|
||||||
|
typedef void *EGLDisplay;
|
||||||
|
typedef void *EGLImage;
|
||||||
|
#define EGL_MAX_PLANES 4
|
||||||
|
|
||||||
|
class EGLExtensions {
|
||||||
|
public:
|
||||||
|
EGLExtensions(EGLDisplay dpy);
|
||||||
|
~EGLExtensions() {}
|
||||||
|
bool isSupported(const QString &extension) const;
|
||||||
|
private:
|
||||||
|
const QStringList m_Extensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
#define RENDERER_ATTRIBUTE_FULLSCREEN_ONLY 0x01
|
#define RENDERER_ATTRIBUTE_FULLSCREEN_ONLY 0x01
|
||||||
#define RENDERER_ATTRIBUTE_1080P_MAX 0x02
|
#define RENDERER_ATTRIBUTE_1080P_MAX 0x02
|
||||||
|
|
||||||
@@ -68,4 +85,25 @@ public:
|
|||||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
|
||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
// By default we can't do EGL
|
||||||
|
virtual bool canExportEGL() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool initializeEGL(EGLDisplay,
|
||||||
|
const EGLExtensions &) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ssize_t exportEGLImages(AVFrame *,
|
||||||
|
EGLDisplay,
|
||||||
|
EGLImage[EGL_MAX_PLANES]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the ressources allocated during the last `exportEGLImages` call
|
||||||
|
virtual void freeEGLImages(EGLDisplay, EGLImage[EGL_MAX_PLANES]) {}
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -177,14 +177,22 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the video region size, scaling to fill the window while
|
// SDL_CreateRenderer() can end up having to recreate our window (SDL_RecreateWindow())
|
||||||
|
// to ensure it's compatible with the renderer's OpenGL context. If that happens, we
|
||||||
|
// can get spurious SDL_WINDOWEVENT events that will cause us to (again) recreate our
|
||||||
|
// renderer. This can lead to an infinite to renderer recreation, so discard all
|
||||||
|
// SDL_WINDOWEVENT events after SDL_CreateRenderer().
|
||||||
|
SDL_PumpEvents();
|
||||||
|
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||||
|
|
||||||
|
// Calculate the video region size, scaling to fill the output size while
|
||||||
// preserving the aspect ratio of the video stream.
|
// preserving the aspect ratio of the video stream.
|
||||||
SDL_Rect src, dst;
|
SDL_Rect src, dst;
|
||||||
src.x = src.y = 0;
|
src.x = src.y = 0;
|
||||||
src.w = params->width;
|
src.w = params->width;
|
||||||
src.h = params->height;
|
src.h = params->height;
|
||||||
dst.x = dst.y = 0;
|
dst.x = dst.y = 0;
|
||||||
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
|
SDL_GetRendererOutputSize(m_Renderer, &dst.w, &dst.h);
|
||||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
// Ensure the viewport is set to the desired video region
|
// Ensure the viewport is set to the desired video region
|
||||||
@@ -221,10 +229,10 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type)
|
|||||||
|
|
||||||
if (type == Overlay::OverlayStatusUpdate) {
|
if (type == Overlay::OverlayStatusUpdate) {
|
||||||
// Bottom Left
|
// Bottom Left
|
||||||
int unused;
|
SDL_Rect viewportRect;
|
||||||
SDL_RenderGetLogicalSize(m_Renderer, &unused, &m_OverlayRects[type].y);
|
SDL_RenderGetViewport(m_Renderer, &viewportRect);
|
||||||
m_OverlayRects[type].x = 0;
|
m_OverlayRects[type].x = 0;
|
||||||
m_OverlayRects[type].y -= surface->h;
|
m_OverlayRects[type].y = viewportRect.h - surface->h;
|
||||||
}
|
}
|
||||||
else if (type == Overlay::OverlayDebug) {
|
else if (type == Overlay::OverlayDebug) {
|
||||||
// Top left
|
// Top left
|
||||||
@@ -251,6 +259,11 @@ void SdlRenderer::renderFrame(AVFrame* frame)
|
|||||||
int err;
|
int err;
|
||||||
AVFrame* swFrame = nullptr;
|
AVFrame* swFrame = nullptr;
|
||||||
|
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (frame->hw_frames_ctx != nullptr) {
|
if (frame->hw_frames_ctx != nullptr) {
|
||||||
// If we are acting as the frontend for a hardware
|
// If we are acting as the frontend for a hardware
|
||||||
// accelerated decoder, we'll need to read the frame
|
// accelerated decoder, we'll need to read the frame
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include <streaming/streamutils.h>
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
#include <SDL_egl.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -14,7 +17,11 @@ VAAPIRenderer::VAAPIRenderer()
|
|||||||
m_DrmFd(-1),
|
m_DrmFd(-1),
|
||||||
m_BlacklistedForDirectRendering(false)
|
m_BlacklistedForDirectRendering(false)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
m_PrimeDescriptor.num_layers = 0;
|
||||||
|
m_PrimeDescriptor.num_objects = 0;
|
||||||
|
m_EGLExtDmaBuf = false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
VAAPIRenderer::~VAAPIRenderer()
|
VAAPIRenderer::~VAAPIRenderer()
|
||||||
@@ -38,39 +45,6 @@ VAAPIRenderer::~VAAPIRenderer()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
|
||||||
VAAPIRenderer::validateDriver(VADisplay display)
|
|
||||||
{
|
|
||||||
const char* vendorString = vaQueryVendorString(display);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Validating VAAPI driver: %s",
|
|
||||||
vendorString ? vendorString : "<unknown>");
|
|
||||||
|
|
||||||
if (isDirectRenderingSupported()) {
|
|
||||||
int entrypointCount;
|
|
||||||
VAEntrypoint entrypoints[vaMaxNumEntrypoints(display)];
|
|
||||||
VAStatus status = vaQueryConfigEntrypoints(display, VAProfileNone, entrypoints, &entrypointCount);
|
|
||||||
if (status == VA_STATUS_SUCCESS) {
|
|
||||||
for (int i = 0; i < entrypointCount; i++) {
|
|
||||||
// Without VAEntrypointVideoProc support, the driver will crash inside vaPutSurface()
|
|
||||||
if (entrypoints[i] == VAEntrypointVideoProc) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"VAEntrypointVideoProc is supported");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"VAAPI driver doesn't support VAEntrypointVideoProc!");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Indirect rendering can use any driver
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VADisplay
|
VADisplay
|
||||||
VAAPIRenderer::openDisplay(SDL_Window* window)
|
VAAPIRenderer::openDisplay(SDL_Window* window)
|
||||||
{
|
{
|
||||||
@@ -193,11 +167,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
if (status == VA_STATUS_SUCCESS && !validateDriver(vaDeviceContext->display)) {
|
|
||||||
vaTerminate(vaDeviceContext->display);
|
|
||||||
vaDeviceContext->display = openDisplay(params->window);
|
|
||||||
status = VA_STATUS_ERROR_UNSUPPORTED_PROFILE;
|
|
||||||
}
|
|
||||||
if (status != VA_STATUS_SUCCESS && qEnvironmentVariableIsEmpty("LIBVA_DRIVER_NAME")) {
|
if (status != VA_STATUS_SUCCESS && qEnvironmentVariableIsEmpty("LIBVA_DRIVER_NAME")) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Trying fallback VAAPI driver names");
|
"Trying fallback VAAPI driver names");
|
||||||
@@ -212,11 +181,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
// always seem to be the case for some reason.
|
// always seem to be the case for some reason.
|
||||||
qputenv("LIBVA_DRIVER_NAME", "iHD");
|
qputenv("LIBVA_DRIVER_NAME", "iHD");
|
||||||
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
if (status == VA_STATUS_SUCCESS && !validateDriver(vaDeviceContext->display)) {
|
|
||||||
vaTerminate(vaDeviceContext->display);
|
|
||||||
vaDeviceContext->display = openDisplay(params->window);
|
|
||||||
status = VA_STATUS_ERROR_UNSUPPORTED_PROFILE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status != VA_STATUS_SUCCESS) {
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
@@ -225,11 +189,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
// explicitly try i965 to handle this case.
|
// explicitly try i965 to handle this case.
|
||||||
qputenv("LIBVA_DRIVER_NAME", "i965");
|
qputenv("LIBVA_DRIVER_NAME", "i965");
|
||||||
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
if (status == VA_STATUS_SUCCESS && !validateDriver(vaDeviceContext->display)) {
|
|
||||||
vaTerminate(vaDeviceContext->display);
|
|
||||||
vaDeviceContext->display = openDisplay(params->window);
|
|
||||||
status = VA_STATUS_ERROR_UNSUPPORTED_PROFILE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status != VA_STATUS_SUCCESS) {
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
@@ -237,11 +196,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
// so try it too if all else fails.
|
// so try it too if all else fails.
|
||||||
qputenv("LIBVA_DRIVER_NAME", "radeonsi");
|
qputenv("LIBVA_DRIVER_NAME", "radeonsi");
|
||||||
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
status = vaInitialize(vaDeviceContext->display, &major, &minor);
|
||||||
if (status == VA_STATUS_SUCCESS && !validateDriver(vaDeviceContext->display)) {
|
|
||||||
vaTerminate(vaDeviceContext->display);
|
|
||||||
vaDeviceContext->display = openDisplay(params->window);
|
|
||||||
status = VA_STATUS_ERROR_UNSUPPORTED_PROFILE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status != VA_STATUS_SUCCESS) {
|
if (status != VA_STATUS_SUCCESS) {
|
||||||
@@ -369,9 +323,43 @@ VAAPIRenderer::needsTestFrame()
|
|||||||
bool
|
bool
|
||||||
VAAPIRenderer::isDirectRenderingSupported()
|
VAAPIRenderer::isDirectRenderingSupported()
|
||||||
{
|
{
|
||||||
// Many Wayland renderers don't support YUV surfaces, so use
|
if (qgetenv("VAAPI_FORCE_DIRECT") == "1") {
|
||||||
// another frontend renderer to draw our frames.
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
return m_WindowSystem == SDL_SYSWM_X11 && !m_BlacklistedForDirectRendering;
|
"Using direct rendering due to environment variable");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (qgetenv("VAAPI_FORCE_INDIRECT") == "1") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using indirect rendering due to environment variable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only support direct rendering on X11 with VAEntrypointVideoProc support
|
||||||
|
if (m_WindowSystem != SDL_SYSWM_X11 || m_BlacklistedForDirectRendering) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using indirect rendering due to WM or blacklist");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
VAEntrypoint entrypoints[vaMaxNumEntrypoints(vaDeviceContext->display)];
|
||||||
|
int entrypointCount;
|
||||||
|
VAStatus status = vaQueryConfigEntrypoints(vaDeviceContext->display, VAProfileNone, entrypoints, &entrypointCount);
|
||||||
|
if (status == VA_STATUS_SUCCESS) {
|
||||||
|
for (int i = 0; i < entrypointCount; i++) {
|
||||||
|
// Without VAEntrypointVideoProc support, the driver will crash inside vaPutSurface()
|
||||||
|
if (entrypoints[i] == VAEntrypointVideoProc) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using direct rendering with VAEntrypointVideoProc");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using indirect rendering due to lack of VAEntrypointVideoProc");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int VAAPIRenderer::getDecoderColorspace()
|
int VAAPIRenderer::getDecoderColorspace()
|
||||||
@@ -384,6 +372,11 @@ int VAAPIRenderer::getDecoderColorspace()
|
|||||||
void
|
void
|
||||||
VAAPIRenderer::renderFrame(AVFrame* frame)
|
VAAPIRenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||||
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
@@ -442,3 +435,173 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
|
|||||||
SDL_assert(false);
|
SDL_assert(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
|
||||||
|
// Ensure that vaExportSurfaceHandle() is supported by the VA-API driver
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::canExportEGL() {
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
|
||||||
|
VASurfaceID surfaceId;
|
||||||
|
VAStatus st;
|
||||||
|
VADRMPRIMESurfaceDescriptor descriptor;
|
||||||
|
VASurfaceAttrib attrs[2];
|
||||||
|
int attributeCount = 0;
|
||||||
|
|
||||||
|
if (qgetenv("VAAPI_FORCE_DIRECT") == "1") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using direct rendering due to environment variable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFmpeg handles setting these quirk flags for us
|
||||||
|
if (!(vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE)) {
|
||||||
|
attrs[attributeCount].type = VASurfaceAttribMemoryType;
|
||||||
|
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
|
||||||
|
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
|
||||||
|
attrs[attributeCount].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_VA;
|
||||||
|
attributeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These attributes are required for i965 to create a surface that can
|
||||||
|
// be successfully exported via vaExportSurfaceHandle(). iHD doesn't
|
||||||
|
// need these, but it doesn't seem to hurt either.
|
||||||
|
attrs[attributeCount].type = VASurfaceAttribPixelFormat;
|
||||||
|
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
|
||||||
|
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
|
||||||
|
attrs[attributeCount].value.value.i = VA_FOURCC_NV12;
|
||||||
|
attributeCount++;
|
||||||
|
|
||||||
|
st = vaCreateSurfaces(vaDeviceContext->display,
|
||||||
|
VA_RT_FORMAT_YUV420,
|
||||||
|
1280,
|
||||||
|
720,
|
||||||
|
&surfaceId,
|
||||||
|
1,
|
||||||
|
attrs,
|
||||||
|
attributeCount);
|
||||||
|
if (st != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaCreateSurfaces() failed: %d", st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
st = vaExportSurfaceHandle(vaDeviceContext->display,
|
||||||
|
surfaceId,
|
||||||
|
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
|
||||||
|
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
|
||||||
|
&descriptor);
|
||||||
|
|
||||||
|
vaDestroySurfaces(vaDeviceContext->display, &surfaceId, 1);
|
||||||
|
|
||||||
|
if (st != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaExportSurfaceHandle() failed: %d", st);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < descriptor.num_objects; ++i) {
|
||||||
|
close(descriptor.objects[i].fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VAAPI driver supports exporting DRM PRIME surface handles");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
VAAPIRenderer::initializeEGL(EGLDisplay,
|
||||||
|
const EGLExtensions &ext) {
|
||||||
|
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VAAPI-EGL: DMABUF unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t
|
||||||
|
VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
||||||
|
EGLImage images[EGL_MAX_PLANES]) {
|
||||||
|
ssize_t count = 0;
|
||||||
|
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||||
|
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)hwFrameCtx->device_ctx->hwctx;
|
||||||
|
|
||||||
|
VASurfaceID surface_id = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||||
|
VAStatus st = vaExportSurfaceHandle(vaDeviceContext->display,
|
||||||
|
surface_id,
|
||||||
|
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
|
||||||
|
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
|
||||||
|
&m_PrimeDescriptor);
|
||||||
|
if (st != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaExportSurfaceHandle failed: %d", st);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(m_PrimeDescriptor.num_layers <= EGL_MAX_PLANES);
|
||||||
|
|
||||||
|
st = vaSyncSurface(vaDeviceContext->display, surface_id);
|
||||||
|
if (st != VA_STATUS_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"vaSyncSurface failed: %d", st);
|
||||||
|
goto sync_fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
|
||||||
|
const auto &layer = m_PrimeDescriptor.layers[i];
|
||||||
|
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
|
||||||
|
|
||||||
|
EGLAttrib attribs[17] = {
|
||||||
|
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
|
||||||
|
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
|
||||||
|
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
|
||||||
|
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
|
||||||
|
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
|
||||||
|
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
|
||||||
|
EGL_NONE,
|
||||||
|
};
|
||||||
|
if (m_EGLExtDmaBuf) {
|
||||||
|
const EGLAttrib extra[] = {
|
||||||
|
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
|
||||||
|
(EGLint)object.drm_format_modifier,
|
||||||
|
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
|
||||||
|
(EGLint)(object.drm_format_modifier >> 32),
|
||||||
|
EGL_NONE,
|
||||||
|
};
|
||||||
|
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
|
||||||
|
}
|
||||||
|
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||||
|
EGL_LINUX_DMA_BUF_EXT,
|
||||||
|
nullptr, attribs);
|
||||||
|
if (!images[i]) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"eglCreateImage() Failed: %d", eglGetError());
|
||||||
|
goto create_image_fail;
|
||||||
|
}
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
|
||||||
|
create_image_fail:
|
||||||
|
m_PrimeDescriptor.num_layers = count;
|
||||||
|
sync_fail:
|
||||||
|
freeEGLImages(dpy, images);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||||
|
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
|
||||||
|
eglDestroyImage(dpy, images[i]);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
|
||||||
|
close(m_PrimeDescriptor.objects[i].fd);
|
||||||
|
}
|
||||||
|
m_PrimeDescriptor.num_layers = 0;
|
||||||
|
m_PrimeDescriptor.num_objects = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ extern "C" {
|
|||||||
#include <va/va_drm.h>
|
#include <va/va_drm.h>
|
||||||
#endif
|
#endif
|
||||||
#include <libavutil/hwcontext_vaapi.h>
|
#include <libavutil/hwcontext_vaapi.h>
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
#include <va/va_drmcommon.h>
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
class VAAPIRenderer : public IFFmpegRenderer
|
class VAAPIRenderer : public IFFmpegRenderer
|
||||||
@@ -39,9 +42,14 @@ public:
|
|||||||
virtual bool needsTestFrame() override;
|
virtual bool needsTestFrame() override;
|
||||||
virtual bool isDirectRenderingSupported() override;
|
virtual bool isDirectRenderingSupported() override;
|
||||||
virtual int getDecoderColorspace() override;
|
virtual int getDecoderColorspace() override;
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
virtual bool canExportEGL() override;
|
||||||
|
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||||
|
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||||
|
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool validateDriver(VADisplay display);
|
|
||||||
VADisplay openDisplay(SDL_Window* window);
|
VADisplay openDisplay(SDL_Window* window);
|
||||||
|
|
||||||
int m_WindowSystem;
|
int m_WindowSystem;
|
||||||
@@ -57,4 +65,9 @@ private:
|
|||||||
int m_VideoHeight;
|
int m_VideoHeight;
|
||||||
int m_DisplayWidth;
|
int m_DisplayWidth;
|
||||||
int m_DisplayHeight;
|
int m_DisplayHeight;
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
|
||||||
|
bool m_EGLExtDmaBuf;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,6 +302,11 @@ int VDPAURenderer::getDecoderColorspace()
|
|||||||
|
|
||||||
void VDPAURenderer::renderFrame(AVFrame* frame)
|
void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
VdpStatus status;
|
VdpStatus status;
|
||||||
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
#import <dispatch/dispatch.h>
|
#import <dispatch/dispatch.h>
|
||||||
#import <Metal/Metal.h>
|
#import <Metal/Metal.h>
|
||||||
|
|
||||||
|
@interface VTView : NSView
|
||||||
|
- (NSView *)hitTest:(NSPoint)point;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation VTView
|
||||||
|
|
||||||
|
- (NSView *)hitTest:(NSPoint)point {
|
||||||
|
Q_UNUSED(point);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
class VTRenderer : public IFFmpegRenderer
|
class VTRenderer : public IFFmpegRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -145,6 +158,11 @@ public:
|
|||||||
// Caller frees frame after we return
|
// Caller frees frame after we return
|
||||||
virtual void renderFrame(AVFrame* frame) override
|
virtual void renderFrame(AVFrame* frame) override
|
||||||
{
|
{
|
||||||
|
if (frame == nullptr) {
|
||||||
|
// End of stream - nothing to do for us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
||||||
|
|
||||||
@@ -264,7 +282,7 @@ public:
|
|||||||
CMSampleTimingInfo timingInfo = {
|
CMSampleTimingInfo timingInfo = {
|
||||||
.duration = kCMTimeInvalid,
|
.duration = kCMTimeInvalid,
|
||||||
.decodeTimeStamp = kCMTimeInvalid,
|
.decodeTimeStamp = kCMTimeInvalid,
|
||||||
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
|
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time())
|
||||||
};
|
};
|
||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
CMSampleBufferRef sampleBuffer;
|
||||||
@@ -398,7 +416,7 @@ public:
|
|||||||
// SDL adds its own content view to listen for events.
|
// SDL adds its own content view to listen for events.
|
||||||
// We need to add a subview for our display layer.
|
// We need to add a subview for our display layer.
|
||||||
NSView* contentView = info.info.cocoa.window.contentView;
|
NSView* contentView = info.info.cocoa.window.contentView;
|
||||||
m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
|
m_StreamView = [[VTView alloc] initWithFrame:contentView.bounds];
|
||||||
|
|
||||||
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||||
m_DisplayLayer.bounds = m_StreamView.bounds;
|
m_DisplayLayer.bounds = m_StreamView.bounds;
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
#include "ffmpeg-renderers/drm.h"
|
#include "ffmpeg-renderers/drm.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
#include "ffmpeg-renderers/eglvid.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
// This is gross but it allows us to use sizeof()
|
// This is gross but it allows us to use sizeof()
|
||||||
#include "ffmpeg_videosamples.cpp"
|
#include "ffmpeg_videosamples.cpp"
|
||||||
|
|
||||||
@@ -190,6 +194,16 @@ void FFmpegVideoDecoder::reset()
|
|||||||
|
|
||||||
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
if (m_BackendRenderer->canExportEGL()) {
|
||||||
|
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
|
||||||
|
if (m_FrontendRenderer->initialize(params)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete m_FrontendRenderer;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (m_BackendRenderer->isDirectRenderingSupported()) {
|
if (m_BackendRenderer->isDirectRenderingSupported()) {
|
||||||
// The backend renderer can render to the display
|
// The backend renderer can render to the display
|
||||||
m_FrontendRenderer = m_BackendRenderer;
|
m_FrontendRenderer = m_BackendRenderer;
|
||||||
@@ -409,17 +423,45 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
|||||||
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
|
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
|
||||||
{
|
{
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
const char* codecString;
|
||||||
|
|
||||||
// Start with an empty string
|
// Start with an empty string
|
||||||
output[offset] = 0;
|
output[offset] = 0;
|
||||||
|
|
||||||
|
switch (m_VideoFormat)
|
||||||
|
{
|
||||||
|
case VIDEO_FORMAT_H264:
|
||||||
|
codecString = "H.264";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIDEO_FORMAT_H265:
|
||||||
|
codecString = "HEVC";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIDEO_FORMAT_H265_MAIN10:
|
||||||
|
codecString = "HEVC Main 10";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
codecString = "UNKNOWN";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (stats.receivedFps > 0) {
|
if (stats.receivedFps > 0) {
|
||||||
|
if (m_VideoDecoderCtx != nullptr) {
|
||||||
|
offset += sprintf(&output[offset],
|
||||||
|
"Video stream: %dx%d %.2f FPS (Codec: %s)\n",
|
||||||
|
m_VideoDecoderCtx->width,
|
||||||
|
m_VideoDecoderCtx->height,
|
||||||
|
stats.totalFps,
|
||||||
|
codecString);
|
||||||
|
}
|
||||||
|
|
||||||
offset += sprintf(&output[offset],
|
offset += sprintf(&output[offset],
|
||||||
"Estimated host PC frame rate: %.2f FPS\n"
|
|
||||||
"Incoming frame rate from network: %.2f FPS\n"
|
"Incoming frame rate from network: %.2f FPS\n"
|
||||||
"Decoding frame rate: %.2f FPS\n"
|
"Decoding frame rate: %.2f FPS\n"
|
||||||
"Rendering frame rate: %.2f FPS\n",
|
"Rendering frame rate: %.2f FPS\n",
|
||||||
stats.totalFps,
|
|
||||||
stats.receivedFps,
|
stats.receivedFps,
|
||||||
stats.decodedFps,
|
stats.decodedFps,
|
||||||
stats.renderedFps);
|
stats.renderedFps);
|
||||||
@@ -842,6 +884,13 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
||||||
m_Pkt.size = offset;
|
m_Pkt.size = offset;
|
||||||
|
|
||||||
|
if (du->frameType == FRAME_TYPE_IDR) {
|
||||||
|
m_Pkt.flags = AV_PKT_FLAG_KEY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_Pkt.flags = 0;
|
||||||
|
}
|
||||||
|
|
||||||
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
|
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
|
||||||
|
|
||||||
Uint32 beforeDecode = SDL_GetTicks();
|
Uint32 beforeDecode = SDL_GetTicks();
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.0.0
|
2.2.0
|
||||||
+6
-5
@@ -5,12 +5,12 @@ clone_depth: 1
|
|||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||||
QTDIR: C:\Qt\5.14\msvc2017_64
|
QTDIR: C:\Qt\5.15\msvc2019_64
|
||||||
ARCH: x64
|
ARCH: x64
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: macOS
|
- APPVEYOR_BUILD_WORKER_IMAGE: macOS
|
||||||
BUILD_TARGET: macos
|
BUILD_TARGET: macos
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||||
QTDIR: C:\Qt\5.14\msvc2017
|
QTDIR: C:\Qt\5.15\msvc2019
|
||||||
ARCH: x86
|
ARCH: x86
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
|
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
|
||||||
BUILD_TARGET: steamlink
|
BUILD_TARGET: steamlink
|
||||||
@@ -21,6 +21,8 @@ environment:
|
|||||||
install:
|
install:
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
|
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
|
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
|
||||||
|
- sh: '[ "$BUILD_TARGET" != macos ] || brew install qt'
|
||||||
|
- sh: '[ "$BUILD_TARGET" != macos ] || brew link --force qt'
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial'
|
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial'
|
||||||
@@ -28,9 +30,9 @@ install:
|
|||||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
|
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.12 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.12 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.7.1 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.8.0 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.11 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.11 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.2.2 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
|
||||||
|
|
||||||
before_build:
|
before_build:
|
||||||
@@ -48,7 +50,6 @@ build_script:
|
|||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
|
||||||
|
|
||||||
after_build:
|
after_build:
|
||||||
- cmd: 'appveyor PushArtifact build\installer-%ARCH%-Release\MoonlightSetup-%ARCH%-%VERSION%.exe -FileName MoonlightSetup-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.exe'
|
|
||||||
- cmd: 'appveyor PushArtifact build\installer-%ARCH%-Release\MoonlightPortable-%ARCH%-%VERSION%.zip -FileName MoonlightPortable-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
|
- cmd: 'appveyor PushArtifact build\installer-%ARCH%-Release\MoonlightPortable-%ARCH%-%VERSION%.zip -FileName MoonlightPortable-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
|
||||||
- cmd: 'appveyor PushArtifact build\symbols-%ARCH%-Release\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip -FileName MoonlightDebuggingSymbols-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
|
- cmd: 'appveyor PushArtifact build\symbols-%ARCH%-Release\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip -FileName MoonlightDebuggingSymbols-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || appveyor PushArtifact build/installer-release/Moonlight-SteamLink-$VERSION.zip -FileName Moonlight-SteamLink-r$APPVEYOR_BUILD_NUMBER.zip'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || appveyor PushArtifact build/installer-release/Moonlight-SteamLink-$VERSION.zip -FileName Moonlight-SteamLink-r$APPVEYOR_BUILD_NUMBER.zip'
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
SOURCES = main.cpp
|
||||||
|
|
||||||
|
CONFIG += link_pkgconfig
|
||||||
|
|
||||||
|
# This adds /opt/vc/include to the include path which
|
||||||
|
# pulls in the BRCM GLES and EGL libraries. If we don't
|
||||||
|
# add this, we'll get the system's headers which expose
|
||||||
|
# functionality the runtime GL implementation won't have.
|
||||||
|
packagesExist(mmal) {
|
||||||
|
PKGCONFIG += mmal
|
||||||
|
}
|
||||||
|
|
||||||
|
PKGCONFIG += sdl2 egl libavcodec libavutil
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include <SDL_egl.h>
|
||||||
|
#include <SDL_opengles2.h>
|
||||||
|
|
||||||
|
#ifndef EGL_VERSION_1_4
|
||||||
|
#error EGLRenderer requires EGL 1.4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef GL_ES_VERSION_2_0
|
||||||
|
#error EGLRenderer requires OpenGL ES 2.0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+3
-2
@@ -13,5 +13,6 @@ CONFIG(release, debug|release) {
|
|||||||
#CONFIG += sanitizer sanitize_address
|
#CONFIG += sanitizer sanitize_address
|
||||||
|
|
||||||
# Enable ASan for Windows
|
# Enable ASan for Windows
|
||||||
#QMAKE_CFLAGS += /fsanitize=address /MD
|
#QMAKE_CFLAGS += -fsanitize=address
|
||||||
#LIBS += clang_rt.asan_dynamic-i386.lib clang_rt.asan_dynamic_runtime_thunk-i386.lib
|
#QMAKE_CXXFLAGS += -fsanitize=address
|
||||||
|
#QMAKE_LFLAGS += -incremental:no -wholearchive:clang_rt.asan_dynamic-x86_64.lib -wholearchive:clang_rt.asan_dynamic_runtime_thunk-x86_64.lib
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BuildMachineOSBuild</key>
|
<key>BuildMachineOSBuild</key>
|
||||||
<string>19E287</string>
|
<string>19G2021</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -27,17 +27,17 @@
|
|||||||
<key>DTCompiler</key>
|
<key>DTCompiler</key>
|
||||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
<key>DTPlatformBuild</key>
|
<key>DTPlatformBuild</key>
|
||||||
<string>11C505</string>
|
<string>11E801a</string>
|
||||||
<key>DTPlatformVersion</key>
|
<key>DTPlatformVersion</key>
|
||||||
<string>GM</string>
|
<string>GM</string>
|
||||||
<key>DTSDKBuild</key>
|
<key>DTSDKBuild</key>
|
||||||
<string>19B90</string>
|
<string>19G68</string>
|
||||||
<key>DTSDKName</key>
|
<key>DTSDKName</key>
|
||||||
<string>macosx10.15</string>
|
<string>macosx10.15</string>
|
||||||
<key>DTXcode</key>
|
<key>DTXcode</key>
|
||||||
<string>1130</string>
|
<string>1170</string>
|
||||||
<key>DTXcodeBuild</key>
|
<key>DTXcodeBuild</key>
|
||||||
<string>11C505</string>
|
<string>11E801a</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.6</string>
|
<string>10.6</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@
|
|||||||
</data>
|
</data>
|
||||||
<key>Resources/Info.plist</key>
|
<key>Resources/Info.plist</key>
|
||||||
<data>
|
<data>
|
||||||
xIjchI5RXIIqR9iBwzwdhWVeJmQ=
|
k9vWJij3RA8s13h/WkHnqVv977U=
|
||||||
</data>
|
</data>
|
||||||
<key>Resources/LICENSE-bsd.txt</key>
|
<key>Resources/LICENSE-bsd.txt</key>
|
||||||
<data>
|
<data>
|
||||||
@@ -57,11 +57,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
xIjchI5RXIIqR9iBwzwdhWVeJmQ=
|
k9vWJij3RA8s13h/WkHnqVv977U=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
8Rqw3j5YT+AoPOKV4JE3RuVPPDb4jHtT4tpmG0phLYc=
|
WME8EBqUAByTxhCypUOOYXVJiElcUNSkBXR1bxH3n5Q=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Resources/LICENSE-bsd.txt</key>
|
<key>Resources/LICENSE-bsd.txt</key>
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -59,6 +59,7 @@
|
|||||||
#include "SDL_timer.h"
|
#include "SDL_timer.h"
|
||||||
#include "SDL_version.h"
|
#include "SDL_version.h"
|
||||||
#include "SDL_video.h"
|
#include "SDL_video.h"
|
||||||
|
#include "SDL_locale.h"
|
||||||
|
|
||||||
#include "begin_code.h"
|
#include "begin_code.h"
|
||||||
/* Set up for C function definitions, even when using C++ */
|
/* Set up for C function definitions, even when using C++ */
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ assert can have unique static variables associated with it.
|
|||||||
#define SDL_TriggerBreakpoint() __debugbreak()
|
#define SDL_TriggerBreakpoint() __debugbreak()
|
||||||
#elif ( (!defined(__NACL__)) && ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))) )
|
#elif ( (!defined(__NACL__)) && ((defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))) )
|
||||||
#define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "int $3\n\t" )
|
#define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "int $3\n\t" )
|
||||||
|
#elif ( defined(__APPLE__) && defined(__arm64__) ) /* this might work on other ARM targets, but this is a known quantity... */
|
||||||
|
#define SDL_TriggerBreakpoint() __asm__ __volatile__ ( "brk #22\n\t" )
|
||||||
#elif defined(__386__) && defined(__WATCOMC__)
|
#elif defined(__386__) && defined(__WATCOMC__)
|
||||||
#define SDL_TriggerBreakpoint() { _asm { int 0x03 } }
|
#define SDL_TriggerBreakpoint() { _asm { int 0x03 } }
|
||||||
#elif defined(HAVE_SIGNAL_H) && !defined(__WATCOMC__)
|
#elif defined(HAVE_SIGNAL_H) && !defined(__WATCOMC__)
|
||||||
|
|||||||
@@ -246,10 +246,33 @@ extern DECLSPEC size_t SDLCALL SDL_SIMDGetAlignment(void);
|
|||||||
* \return Pointer to newly-allocated block, NULL if out of memory.
|
* \return Pointer to newly-allocated block, NULL if out of memory.
|
||||||
*
|
*
|
||||||
* \sa SDL_SIMDAlignment
|
* \sa SDL_SIMDAlignment
|
||||||
|
* \sa SDL_SIMDRealloc
|
||||||
* \sa SDL_SIMDFree
|
* \sa SDL_SIMDFree
|
||||||
*/
|
*/
|
||||||
extern DECLSPEC void * SDLCALL SDL_SIMDAlloc(const size_t len);
|
extern DECLSPEC void * SDLCALL SDL_SIMDAlloc(const size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Reallocate memory obtained from SDL_SIMDAlloc
|
||||||
|
*
|
||||||
|
* It is not valid to use this function on a pointer from anything but
|
||||||
|
* SDL_SIMDAlloc(). It can't be used on pointers from malloc, realloc,
|
||||||
|
* SDL_malloc, memalign, new[], etc.
|
||||||
|
*
|
||||||
|
* \param mem The pointer obtained from SDL_SIMDAlloc. This function also
|
||||||
|
* accepts NULL, at which point this function is the same as
|
||||||
|
* calling SDL_realloc with a NULL pointer.
|
||||||
|
* \param len The length, in bytes, of the block to allocated. The actual
|
||||||
|
* allocated block might be larger due to padding, etc. Passing 0
|
||||||
|
* will return a non-NULL pointer, assuming the system isn't out of
|
||||||
|
* memory.
|
||||||
|
* \return Pointer to newly-reallocated block, NULL if out of memory.
|
||||||
|
*
|
||||||
|
* \sa SDL_SIMDAlignment
|
||||||
|
* \sa SDL_SIMDAlloc
|
||||||
|
* \sa SDL_SIMDFree
|
||||||
|
*/
|
||||||
|
extern DECLSPEC void * SDLCALL SDL_SIMDRealloc(void *mem, size_t len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Deallocate memory obtained from SDL_SIMDAlloc
|
* \brief Deallocate memory obtained from SDL_SIMDAlloc
|
||||||
*
|
*
|
||||||
@@ -260,6 +283,7 @@ extern DECLSPEC void * SDLCALL SDL_SIMDAlloc(const size_t len);
|
|||||||
* However, SDL_SIMDFree(NULL) is a legal no-op.
|
* However, SDL_SIMDFree(NULL) is a legal no-op.
|
||||||
*
|
*
|
||||||
* \sa SDL_SIMDAlloc
|
* \sa SDL_SIMDAlloc
|
||||||
|
* \sa SDL_SIMDRealloc
|
||||||
*/
|
*/
|
||||||
extern DECLSPEC void SDLCALL SDL_SIMDFree(void *ptr);
|
extern DECLSPEC void SDLCALL SDL_SIMDFree(void *ptr);
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ typedef enum
|
|||||||
Called on Android in onResume()
|
Called on Android in onResume()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
SDL_LOCALECHANGED, /**< The user's locale preferences have changed. */
|
||||||
|
|
||||||
/* Display events */
|
/* Display events */
|
||||||
SDL_DISPLAYEVENT = 0x150, /**< Display state change */
|
SDL_DISPLAYEVENT = 0x150, /**< Display state change */
|
||||||
|
|
||||||
|
|||||||
@@ -337,21 +337,12 @@ typedef struct _SDL_Haptic SDL_Haptic;
|
|||||||
#define SDL_HAPTIC_SPHERICAL 2
|
#define SDL_HAPTIC_SPHERICAL 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Uses first axis only.
|
* \brief Use this value to play an effect on the steering wheel axis. This
|
||||||
* For some device with only one axis (steering wheel,...),
|
* provides better compatibility across platforms and devices as SDL will guess
|
||||||
* SDL_HAPTIC_CARTESIAN does not work. SDL_HAPTIC_FIRST_AXIS can be used in
|
* the correct axis.
|
||||||
* this case.
|
|
||||||
* Using SDL_HAPTIC_FIRST_AXIS is equivalent to :
|
|
||||||
* \code
|
|
||||||
* SDL_HapticDirection direction;
|
|
||||||
* direction.type = SDL_HAPTIC_CARTESIAN;
|
|
||||||
* direction.dir[0] = 1;
|
|
||||||
* direction.dir[1] = 0;
|
|
||||||
* direction.dir[2] = 0;
|
|
||||||
* \endcode
|
|
||||||
* \sa SDL_HapticDirection
|
* \sa SDL_HapticDirection
|
||||||
*/
|
*/
|
||||||
#define SDL_HAPTIC_FIRST_AXIS 3
|
#define SDL_HAPTIC_STEERING_AXIS 3
|
||||||
|
|
||||||
/* @} *//* Direction encodings */
|
/* @} *//* Direction encodings */
|
||||||
|
|
||||||
@@ -461,7 +452,7 @@ typedef struct _SDL_Haptic SDL_Haptic;
|
|||||||
* \sa SDL_HAPTIC_POLAR
|
* \sa SDL_HAPTIC_POLAR
|
||||||
* \sa SDL_HAPTIC_CARTESIAN
|
* \sa SDL_HAPTIC_CARTESIAN
|
||||||
* \sa SDL_HAPTIC_SPHERICAL
|
* \sa SDL_HAPTIC_SPHERICAL
|
||||||
* \sa SDL_HAPTIC_FIRST_AXIS
|
* \sa SDL_HAPTIC_STEERING_AXIS
|
||||||
* \sa SDL_HapticEffect
|
* \sa SDL_HapticEffect
|
||||||
* \sa SDL_HapticNumAxes
|
* \sa SDL_HapticNumAxes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -369,7 +369,9 @@ extern "C" {
|
|||||||
#define SDL_HINT_MOUSE_TOUCH_EVENTS "SDL_MOUSE_TOUCH_EVENTS"
|
#define SDL_HINT_MOUSE_TOUCH_EVENTS "SDL_MOUSE_TOUCH_EVENTS"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Minimize your SDL_Window if it loses key focus when in fullscreen mode. Defaults to true.
|
* \brief Minimize your SDL_Window if it loses key focus when in fullscreen mode. Defaults to false.
|
||||||
|
* \warning Before SDL 2.0.14, this defaulted to true! In 2.0.14, we're
|
||||||
|
* seeing if "true" causes more problems than it solves in modern times.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#define SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS "SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS"
|
#define SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS "SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS"
|
||||||
@@ -694,6 +696,18 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_JOYSTICK_RAWINPUT "SDL_JOYSTICK_RAWINPUT"
|
#define SDL_HINT_JOYSTICK_RAWINPUT "SDL_JOYSTICK_RAWINPUT"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether Linux joysticks adhere their HID-defined deadzones or return unfiltered values.
|
||||||
|
* This is useful for Wine which implements its own deadzone handler if requested by games, also it enables xinput
|
||||||
|
* games to receive unfiltered values as required from the API.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - Linux deadzones are not used by SDL
|
||||||
|
* "1" - Linux deadzones are used by SDL (the default)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_LINUX_JOYSTICK_DEADZONES "SDL_LINUX_JOYSTICK_DEADZONES"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief If set to "0" then never set the top most bit on a SDL Window, even if the video mode expects it.
|
* \brief If set to "0" then never set the top most bit on a SDL Window, even if the video mode expects it.
|
||||||
* This is a debugging aid for developers and not expected to be used by end users. The default is "1"
|
* This is a debugging aid for developers and not expected to be used by end users. The default is "1"
|
||||||
@@ -763,6 +777,19 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_THREAD_STACK_SIZE "SDL_THREAD_STACK_SIZE"
|
#define SDL_HINT_THREAD_STACK_SIZE "SDL_THREAD_STACK_SIZE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A string specifying additional information to use with SDL_SetThreadPriority.
|
||||||
|
*
|
||||||
|
* By default SDL_SetThreadPriority will make appropriate system changes in order to
|
||||||
|
* apply a thread priority. For example on systems using pthreads the scheduler policy
|
||||||
|
* is changed automatically to a policy that works well with a given priority.
|
||||||
|
* Code which has specific requirements can override SDL's default behavior with this hint.
|
||||||
|
*
|
||||||
|
* pthread hint values are "current", "other", "fifo" and "rr".
|
||||||
|
* Currently no other platform hint values are defined but may be in the future.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_THREAD_PRIORITY_POLICY "SDL_THREAD_PRIORITY_POLICY"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief If set to 1, then do not allow high-DPI windows. ("Retina" on Mac and iOS)
|
* \brief If set to 1, then do not allow high-DPI windows. ("Retina" on Mac and iOS)
|
||||||
*/
|
*/
|
||||||
@@ -1027,6 +1054,21 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT "SDL_EMSCRIPTEN_KEYBOARD_ELEMENT"
|
#define SDL_HINT_EMSCRIPTEN_KEYBOARD_ELEMENT "SDL_EMSCRIPTEN_KEYBOARD_ELEMENT"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Disable giving back control to the browser automatically
|
||||||
|
* when running with asyncify
|
||||||
|
*
|
||||||
|
* With -s ASYNCIFY, SDL2 calls emscripten_sleep during operations
|
||||||
|
* such as refreshing the screen or polling events.
|
||||||
|
*
|
||||||
|
* This hint only applies to the emscripten platform
|
||||||
|
*
|
||||||
|
* The variable can be set to the following values:
|
||||||
|
* "0" - Disable emscripten_sleep calls (if you give back browser control manually or use asyncify for other purposes)
|
||||||
|
* "1" - Enable emscripten_sleep calls (the default)
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_EMSCRIPTEN_ASYNCIFY "SDL_EMSCRIPTEN_ASYNCIFY"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Tell SDL not to catch the SIGINT or SIGTERM signals.
|
* \brief Tell SDL not to catch the SIGINT or SIGTERM signals.
|
||||||
*
|
*
|
||||||
@@ -1292,7 +1334,7 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_WAVE_FACT_CHUNK "SDL_WAVE_FACT_CHUNK"
|
#define SDL_HINT_WAVE_FACT_CHUNK "SDL_WAVE_FACT_CHUNK"
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* \brief Override for SDL_GetDisplayUsableBounds()
|
* \brief Override for SDL_GetDisplayUsableBounds()
|
||||||
*
|
*
|
||||||
* If set, this hint will override the expected results for
|
* If set, this hint will override the expected results for
|
||||||
@@ -1306,6 +1348,63 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_DISPLAY_USABLE_BOUNDS "SDL_DISPLAY_USABLE_BOUNDS"
|
#define SDL_HINT_DISPLAY_USABLE_BOUNDS "SDL_DISPLAY_USABLE_BOUNDS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Specify an application name for an audio device.
|
||||||
|
*
|
||||||
|
* Some audio backends (such as PulseAudio) allow you to describe your audio
|
||||||
|
* stream. Among other things, this description might show up in a system
|
||||||
|
* control panel that lets the user adjust the volume on specific audio
|
||||||
|
* streams instead of using one giant master volume slider.
|
||||||
|
*
|
||||||
|
* This hints lets you transmit that information to the OS. The contents of
|
||||||
|
* this hint are used while opening an audio device. You should use a string
|
||||||
|
* that describes your program ("My Game 2: The Revenge")
|
||||||
|
*
|
||||||
|
* Setting this to "" or leaving it unset will have SDL use a reasonable
|
||||||
|
* default: probably the application's name or "SDL Application" if SDL
|
||||||
|
* doesn't have any better information.
|
||||||
|
*
|
||||||
|
* On targets where this is not supported, this hint does nothing.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Specify an application name for an audio device.
|
||||||
|
*
|
||||||
|
* Some audio backends (such as PulseAudio) allow you to describe your audio
|
||||||
|
* stream. Among other things, this description might show up in a system
|
||||||
|
* control panel that lets the user adjust the volume on specific audio
|
||||||
|
* streams instead of using one giant master volume slider.
|
||||||
|
*
|
||||||
|
* This hints lets you transmit that information to the OS. The contents of
|
||||||
|
* this hint are used while opening an audio device. You should use a string
|
||||||
|
* that describes your what your program is playing ("audio stream" is
|
||||||
|
* probably sufficient in many cases, but this could be useful for something
|
||||||
|
* like "team chat" if you have a headset playing VoIP audio separately).
|
||||||
|
*
|
||||||
|
* Setting this to "" or leaving it unset will have SDL use a reasonable
|
||||||
|
* default: "audio stream" or something similar.
|
||||||
|
*
|
||||||
|
* On targets where this is not supported, this hint does nothing.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_AUDIO_DEVICE_STREAM_NAME "SDL_AUDIO_DEVICE_STREAM_NAME"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Override for SDL_GetPreferredLocales()
|
||||||
|
*
|
||||||
|
* If set, this will be favored over anything the OS might report for the
|
||||||
|
* user's preferred locales. Changing this hint at runtime will not generate
|
||||||
|
* a SDL_LOCALECHANGED event (but if you can change the hint, you can push
|
||||||
|
* your own event, if you want).
|
||||||
|
*
|
||||||
|
* The format of this hint is a comma-separated list of language and locale,
|
||||||
|
* combined with an underscore, as is a common format: "en_GB". Locale is
|
||||||
|
* optional: "en". So you might have a list like this: "en_GB,jp,es_PT"
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_PREFERRED_LOCALES "SDL_PREFERRED_LOCALES"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief An enumeration of hint priorities
|
* \brief An enumeration of hint priorities
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -336,13 +336,13 @@ typedef enum
|
|||||||
KMOD_NUM = 0x1000,
|
KMOD_NUM = 0x1000,
|
||||||
KMOD_CAPS = 0x2000,
|
KMOD_CAPS = 0x2000,
|
||||||
KMOD_MODE = 0x4000,
|
KMOD_MODE = 0x4000,
|
||||||
KMOD_RESERVED = 0x8000
|
KMOD_RESERVED = 0x8000,
|
||||||
} SDL_Keymod;
|
|
||||||
|
|
||||||
#define KMOD_CTRL (KMOD_LCTRL|KMOD_RCTRL)
|
KMOD_CTRL = KMOD_LCTRL | KMOD_RCTRL,
|
||||||
#define KMOD_SHIFT (KMOD_LSHIFT|KMOD_RSHIFT)
|
KMOD_SHIFT = KMOD_LSHIFT | KMOD_RSHIFT,
|
||||||
#define KMOD_ALT (KMOD_LALT|KMOD_RALT)
|
KMOD_ALT = KMOD_LALT | KMOD_RALT,
|
||||||
#define KMOD_GUI (KMOD_LGUI|KMOD_RGUI)
|
KMOD_GUI = KMOD_LGUI | KMOD_RGUI,
|
||||||
|
} SDL_Keymod;
|
||||||
|
|
||||||
#endif /* SDL_keycode_h_ */
|
#endif /* SDL_keycode_h_ */
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
Simple DirectMedia Layer
|
||||||
|
Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file SDL_locale.h
|
||||||
|
*
|
||||||
|
* Include file for SDL locale services
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _SDL_locale_h
|
||||||
|
#define _SDL_locale_h
|
||||||
|
|
||||||
|
#include "SDL_stdinc.h"
|
||||||
|
#include "SDL_error.h"
|
||||||
|
|
||||||
|
#include "begin_code.h"
|
||||||
|
/* Set up for C function definitions, even when using C++ */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
/* *INDENT-OFF* */
|
||||||
|
extern "C" {
|
||||||
|
/* *INDENT-ON* */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct SDL_Locale
|
||||||
|
{
|
||||||
|
const char *language; /**< A language name, like "en" for English. */
|
||||||
|
const char *country; /**< A country, like "US" for America. Can be NULL. */
|
||||||
|
} SDL_Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Report the user's preferred locale.
|
||||||
|
*
|
||||||
|
* This returns an array of SDL_Locale structs, the final item zeroed out.
|
||||||
|
* When the caller is done with this array, it should call SDL_free() on
|
||||||
|
* the returned value; all the memory involved is allocated in a single
|
||||||
|
* block, so a single SDL_free() will suffice.
|
||||||
|
*
|
||||||
|
* Returned language strings are in the format xx, where 'xx' is an ISO-639
|
||||||
|
* language specifier (such as "en" for English, "de" for German, etc).
|
||||||
|
* Country strings are in the format YY, where "YY" is an ISO-3166 country
|
||||||
|
* code (such as "US" for the United States, "CA" for Canada, etc). Country
|
||||||
|
* might be NULL if there's no specific guidance on them (so you might get
|
||||||
|
* { "en", "US" } for American English, but { "en", NULL } means "English
|
||||||
|
* language, generically"). Language strings are never NULL, except to
|
||||||
|
* terminate the array.
|
||||||
|
*
|
||||||
|
* Please note that not all of these strings are 2 characters; some are
|
||||||
|
* three or more.
|
||||||
|
*
|
||||||
|
* The returned list of locales are in the order of the user's preference.
|
||||||
|
* For example, a German citizen that is fluent in US English and knows
|
||||||
|
* enough Japanese to navigate around Tokyo might have a list like:
|
||||||
|
* { "de", "en_US", "jp", NULL }. Someone from England might prefer British
|
||||||
|
* English (where "color" is spelled "colour", etc), but will settle for
|
||||||
|
* anything like it: { "en_GB", "en", NULL }.
|
||||||
|
*
|
||||||
|
* This function returns NULL on error, including when the platform does not
|
||||||
|
* supply this information at all.
|
||||||
|
*
|
||||||
|
* This might be a "slow" call that has to query the operating system. It's
|
||||||
|
* best to ask for this once and save the results. However, this list can
|
||||||
|
* change, usually because the user has changed a system preference outside
|
||||||
|
* of your program; SDL will send an SDL_LOCALECHANGED event in this case,
|
||||||
|
* if possible, and you can call this function again to get an updated copy
|
||||||
|
* of preferred locales.
|
||||||
|
*
|
||||||
|
* \return array of locales, terminated with a locale with a NULL language
|
||||||
|
* field. Will return NULL on error.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_Locale * SDLCALL SDL_GetPreferredLocales(void);
|
||||||
|
|
||||||
|
/* Ends C function definitions when using C++ */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
/* *INDENT-OFF* */
|
||||||
|
}
|
||||||
|
/* *INDENT-ON* */
|
||||||
|
#endif
|
||||||
|
#include "close_code.h"
|
||||||
|
|
||||||
|
#endif /* _SDL_locale_h */
|
||||||
|
|
||||||
|
/* vi: set ts=4 sw=4 expandtab: */
|
||||||
@@ -188,18 +188,22 @@ typedef enum
|
|||||||
SDL_PIXELFORMAT_RGB332 =
|
SDL_PIXELFORMAT_RGB332 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED8, SDL_PACKEDORDER_XRGB,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED8, SDL_PACKEDORDER_XRGB,
|
||||||
SDL_PACKEDLAYOUT_332, 8, 1),
|
SDL_PACKEDLAYOUT_332, 8, 1),
|
||||||
SDL_PIXELFORMAT_RGB444 =
|
SDL_PIXELFORMAT_XRGB4444 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
|
||||||
SDL_PACKEDLAYOUT_4444, 12, 2),
|
SDL_PACKEDLAYOUT_4444, 12, 2),
|
||||||
SDL_PIXELFORMAT_BGR444 =
|
SDL_PIXELFORMAT_RGB444 = SDL_PIXELFORMAT_XRGB4444,
|
||||||
|
SDL_PIXELFORMAT_XBGR4444 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
|
||||||
SDL_PACKEDLAYOUT_4444, 12, 2),
|
SDL_PACKEDLAYOUT_4444, 12, 2),
|
||||||
SDL_PIXELFORMAT_RGB555 =
|
SDL_PIXELFORMAT_BGR444 = SDL_PIXELFORMAT_XBGR4444,
|
||||||
|
SDL_PIXELFORMAT_XRGB1555 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB,
|
||||||
SDL_PACKEDLAYOUT_1555, 15, 2),
|
SDL_PACKEDLAYOUT_1555, 15, 2),
|
||||||
SDL_PIXELFORMAT_BGR555 =
|
SDL_PIXELFORMAT_RGB555 = SDL_PIXELFORMAT_XRGB1555,
|
||||||
|
SDL_PIXELFORMAT_XBGR1555 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR,
|
||||||
SDL_PACKEDLAYOUT_1555, 15, 2),
|
SDL_PACKEDLAYOUT_1555, 15, 2),
|
||||||
|
SDL_PIXELFORMAT_BGR555 = SDL_PIXELFORMAT_XBGR1555,
|
||||||
SDL_PIXELFORMAT_ARGB4444 =
|
SDL_PIXELFORMAT_ARGB4444 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB,
|
||||||
SDL_PACKEDLAYOUT_4444, 16, 2),
|
SDL_PACKEDLAYOUT_4444, 16, 2),
|
||||||
@@ -236,15 +240,17 @@ typedef enum
|
|||||||
SDL_PIXELFORMAT_BGR24 =
|
SDL_PIXELFORMAT_BGR24 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_BGR, 0,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_BGR, 0,
|
||||||
24, 3),
|
24, 3),
|
||||||
SDL_PIXELFORMAT_RGB888 =
|
SDL_PIXELFORMAT_XRGB8888 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XRGB,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XRGB,
|
||||||
SDL_PACKEDLAYOUT_8888, 24, 4),
|
SDL_PACKEDLAYOUT_8888, 24, 4),
|
||||||
|
SDL_PIXELFORMAT_RGB888 = SDL_PIXELFORMAT_XRGB8888,
|
||||||
SDL_PIXELFORMAT_RGBX8888 =
|
SDL_PIXELFORMAT_RGBX8888 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBX,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBX,
|
||||||
SDL_PACKEDLAYOUT_8888, 24, 4),
|
SDL_PACKEDLAYOUT_8888, 24, 4),
|
||||||
SDL_PIXELFORMAT_BGR888 =
|
SDL_PIXELFORMAT_XBGR8888 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XBGR,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XBGR,
|
||||||
SDL_PACKEDLAYOUT_8888, 24, 4),
|
SDL_PACKEDLAYOUT_8888, 24, 4),
|
||||||
|
SDL_PIXELFORMAT_BGR888 = SDL_PIXELFORMAT_XBGR8888,
|
||||||
SDL_PIXELFORMAT_BGRX8888 =
|
SDL_PIXELFORMAT_BGRX8888 =
|
||||||
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRX,
|
SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRX,
|
||||||
SDL_PACKEDLAYOUT_8888, 24, 4),
|
SDL_PACKEDLAYOUT_8888, 24, 4),
|
||||||
|
|||||||
@@ -96,15 +96,7 @@ typedef struct SDL_RWops
|
|||||||
#if defined(__ANDROID__)
|
#if defined(__ANDROID__)
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
void *fileNameRef;
|
void *asset;
|
||||||
void *inputStreamRef;
|
|
||||||
void *readableByteChannelRef;
|
|
||||||
void *readMethod;
|
|
||||||
void *assetFileDescriptorRef;
|
|
||||||
long position;
|
|
||||||
long size;
|
|
||||||
long offset;
|
|
||||||
int fd;
|
|
||||||
} androidio;
|
} androidio;
|
||||||
#elif defined(__WIN32__)
|
#elif defined(__WIN32__)
|
||||||
struct
|
struct
|
||||||
|
|||||||
@@ -577,6 +577,17 @@ extern DECLSPEC char *SDLCALL SDL_iconv_string(const char *tocode,
|
|||||||
/* force builds using Clang's static analysis tools to use literal C runtime
|
/* force builds using Clang's static analysis tools to use literal C runtime
|
||||||
here, since there are possibly tests that are ineffective otherwise. */
|
here, since there are possibly tests that are ineffective otherwise. */
|
||||||
#if defined(__clang_analyzer__) && !defined(SDL_DISABLE_ANALYZE_MACROS)
|
#if defined(__clang_analyzer__) && !defined(SDL_DISABLE_ANALYZE_MACROS)
|
||||||
|
|
||||||
|
/* The analyzer knows about strlcpy even when the system doesn't provide it */
|
||||||
|
#ifndef HAVE_STRLCPY
|
||||||
|
size_t strlcpy(char* dst, const char* src, size_t size);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* The analyzer knows about strlcat even when the system doesn't provide it */
|
||||||
|
#ifndef HAVE_STRLCAT
|
||||||
|
size_t strlcat(char* dst, const char* src, size_t size);
|
||||||
|
#endif
|
||||||
|
|
||||||
#define SDL_malloc malloc
|
#define SDL_malloc malloc
|
||||||
#define SDL_calloc calloc
|
#define SDL_calloc calloc
|
||||||
#define SDL_realloc realloc
|
#define SDL_realloc realloc
|
||||||
@@ -585,16 +596,23 @@ extern DECLSPEC char *SDLCALL SDL_iconv_string(const char *tocode,
|
|||||||
#define SDL_memcpy memcpy
|
#define SDL_memcpy memcpy
|
||||||
#define SDL_memmove memmove
|
#define SDL_memmove memmove
|
||||||
#define SDL_memcmp memcmp
|
#define SDL_memcmp memcmp
|
||||||
#define SDL_strlen strlen
|
|
||||||
#define SDL_strlcpy strlcpy
|
#define SDL_strlcpy strlcpy
|
||||||
#define SDL_strlcat strlcat
|
#define SDL_strlcat strlcat
|
||||||
|
#define SDL_strlen strlen
|
||||||
|
#define SDL_wcslen wcslen
|
||||||
|
#define SDL_wcslcpy wcslcpy
|
||||||
|
#define SDL_wcslcat wcslcat
|
||||||
#define SDL_strdup strdup
|
#define SDL_strdup strdup
|
||||||
|
#define SDL_wcsdup wcsdup
|
||||||
#define SDL_strchr strchr
|
#define SDL_strchr strchr
|
||||||
#define SDL_strrchr strrchr
|
#define SDL_strrchr strrchr
|
||||||
#define SDL_strstr strstr
|
#define SDL_strstr strstr
|
||||||
|
#define SDL_wcsstr wcsstr
|
||||||
#define SDL_strtokr strtok_r
|
#define SDL_strtokr strtok_r
|
||||||
#define SDL_strcmp strcmp
|
#define SDL_strcmp strcmp
|
||||||
|
#define SDL_wcscmp wcscmp
|
||||||
#define SDL_strncmp strncmp
|
#define SDL_strncmp strncmp
|
||||||
|
#define SDL_wcsncmp wcsncmp
|
||||||
#define SDL_strcasecmp strcasecmp
|
#define SDL_strcasecmp strcasecmp
|
||||||
#define SDL_strncasecmp strncasecmp
|
#define SDL_strncasecmp strncasecmp
|
||||||
#define SDL_sscanf sscanf
|
#define SDL_sscanf sscanf
|
||||||
|
|||||||
@@ -198,6 +198,14 @@ extern DECLSPEC int SDLCALL SDL_AndroidGetExternalStorageState(void);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC const char * SDLCALL SDL_AndroidGetExternalStoragePath(void);
|
extern DECLSPEC const char * SDLCALL SDL_AndroidGetExternalStoragePath(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Request permissions at runtime.
|
||||||
|
|
||||||
|
This blocks the calling thread until the permission is granted or
|
||||||
|
denied. Returns SDL_TRUE if the permission was granted.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_AndroidRequestPermission(const char *permission);
|
||||||
|
|
||||||
#endif /* __ANDROID__ */
|
#endif /* __ANDROID__ */
|
||||||
|
|
||||||
/* Platform specific functions for WinRT */
|
/* Platform specific functions for WinRT */
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ typedef unsigned int SDL_TLSID;
|
|||||||
/**
|
/**
|
||||||
* The SDL thread priority.
|
* The SDL thread priority.
|
||||||
*
|
*
|
||||||
|
* SDL will make system changes as necessary in order to apply the thread priority.
|
||||||
|
* Code which attempts to control thread state related to priority should be aware
|
||||||
|
* that calling SDL_SetThreadPriority may alter such state.
|
||||||
|
* SDL_HINT_THREAD_PRIORITY_POLICY can be used to control aspects of this behavior.
|
||||||
|
*
|
||||||
* \note On many systems you require special privileges to set high or time critical priority.
|
* \note On many systems you require special privileges to set high or time critical priority.
|
||||||
*/
|
*/
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|||||||
@@ -204,8 +204,7 @@ extern DECLSPEC void SDLCALL SDL_Vulkan_UnloadLibrary(void);
|
|||||||
*
|
*
|
||||||
* \sa SDL_Vulkan_CreateSurface()
|
* \sa SDL_Vulkan_CreateSurface()
|
||||||
*/
|
*/
|
||||||
extern DECLSPEC SDL_bool SDLCALL SDL_Vulkan_GetInstanceExtensions(
|
extern DECLSPEC SDL_bool SDLCALL SDL_Vulkan_GetInstanceExtensions(SDL_Window *window,
|
||||||
SDL_Window *window,
|
|
||||||
unsigned int *pCount,
|
unsigned int *pCount,
|
||||||
const char **pNames);
|
const char **pNames);
|
||||||
|
|
||||||
@@ -238,8 +237,7 @@ extern DECLSPEC SDL_bool SDLCALL SDL_Vulkan_GetInstanceExtensions(
|
|||||||
*
|
*
|
||||||
* \sa SDL_Vulkan_GetInstanceExtensions()
|
* \sa SDL_Vulkan_GetInstanceExtensions()
|
||||||
*/
|
*/
|
||||||
extern DECLSPEC SDL_bool SDLCALL SDL_Vulkan_CreateSurface(
|
extern DECLSPEC SDL_bool SDLCALL SDL_Vulkan_CreateSurface(SDL_Window *window,
|
||||||
SDL_Window *window,
|
|
||||||
VkInstance instance,
|
VkInstance instance,
|
||||||
VkSurfaceKHR* surface);
|
VkSurfaceKHR* surface);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BuildMachineOSBuild</key>
|
<key>BuildMachineOSBuild</key>
|
||||||
<string>19E287</string>
|
<string>19G2021</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -31,17 +31,17 @@
|
|||||||
<key>DTCompiler</key>
|
<key>DTCompiler</key>
|
||||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
<key>DTPlatformBuild</key>
|
<key>DTPlatformBuild</key>
|
||||||
<string>11C505</string>
|
<string>11E801a</string>
|
||||||
<key>DTPlatformVersion</key>
|
<key>DTPlatformVersion</key>
|
||||||
<string>GM</string>
|
<string>GM</string>
|
||||||
<key>DTSDKBuild</key>
|
<key>DTSDKBuild</key>
|
||||||
<string>19B90</string>
|
<string>19G68</string>
|
||||||
<key>DTSDKName</key>
|
<key>DTSDKName</key>
|
||||||
<string>macosx10.15</string>
|
<string>macosx10.15</string>
|
||||||
<key>DTXcode</key>
|
<key>DTXcode</key>
|
||||||
<string>1130</string>
|
<string>1170</string>
|
||||||
<key>DTXcodeBuild</key>
|
<key>DTXcodeBuild</key>
|
||||||
<string>11C505</string>
|
<string>11E801a</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.6</string>
|
<string>10.6</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>Resources/Info.plist</key>
|
<key>Resources/Info.plist</key>
|
||||||
<data>
|
<data>
|
||||||
kQWsUbAzKB6cDjSyOGRQOM++OWk=
|
sh0oOLkdzcvMFVtkzp3NXS7VQh4=
|
||||||
</data>
|
</data>
|
||||||
<key>Resources/License.txt</key>
|
<key>Resources/License.txt</key>
|
||||||
<data>
|
<data>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</data>
|
</data>
|
||||||
<key>Resources/default.metallib</key>
|
<key>Resources/default.metallib</key>
|
||||||
<data>
|
<data>
|
||||||
dF9gKJnv0WdQs57PRAzLo+EyGAA=
|
awST523PmC36+lehlmfUr/pwZko=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>files2</key>
|
<key>files2</key>
|
||||||
@@ -27,31 +27,31 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>cdhash</key>
|
<key>cdhash</key>
|
||||||
<data>
|
<data>
|
||||||
p2lcvvUyS6Ahs1nLqMaQaVfaN6Y=
|
pFEaabK4ZTzSRMngHXqK2k/a6tE=
|
||||||
</data>
|
</data>
|
||||||
<key>requirement</key>
|
<key>requirement</key>
|
||||||
<string>cdhash H"51df5d325062884d3b15aea099188c86a0e1e261" or cdhash H"a7695cbef5324ba021b359cba8c6906957da37a6"</string>
|
<string>cdhash H"41e662828193db1f141e133758d4455d73697c3d" or cdhash H"a4511a69b2b8653cd244c9e01d7a8ada4fdaead1"</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL.h</key>
|
<key>Headers/SDL.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
y6Zw1zugKvy7YINfHUZioLU3h7c=
|
o3KVp4++KuUyoa+pJDOXTKG80xY=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
GRE94BY3eDOjtR9EDg6HtLEJAZjYaNV+dq4yfaEVj+U=
|
CLsLV07G2/+c6GGLTVZh91FNYojAHOsuglk/oCKmlso=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_assert.h</key>
|
<key>Headers/SDL_assert.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
aeaMLHO4nr+PB14hBzqp6ZOQMqU=
|
FwT1P3mJzGKC9r+NdzApvnBKWB0=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
BvLXUx+Vfr2uygauOVYH8TxUkdOjRrpuC51187UhoCU=
|
P97E6oE7Pm/2Rd/sQI2lfnboN3OaLWfA4fzXc4lHSW0=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_atomic.h</key>
|
<key>Headers/SDL_atomic.h</key>
|
||||||
@@ -146,11 +146,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
/DTS3Ju22TmH5Qrr4Sy4TfDdlcc=
|
YjRdjce9KUBEQg0r9+V22aY6/tU=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
pPVS8mDMD17ThwISdh+NEXGOqQkOOClFoU0ylYgDNSM=
|
rOaeQwxwi97aliYs+g6rDQlSNO8BjTDD9tnx/2CcyjI=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_endian.h</key>
|
<key>Headers/SDL_endian.h</key>
|
||||||
@@ -179,11 +179,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
K/UpbzGqq8vPXCNo//37Qod99bU=
|
06e1jUIsAc9O803iXrh2jmdbXuM=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
ndL6QYSkZleJN4Va0neG0C1cytQi6sN2YRBZNqg0KGU=
|
6pGmPxZapBntgteeWW4BRZnhLf1R4CvlFsk60Xd3BPs=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_filesystem.h</key>
|
<key>Headers/SDL_filesystem.h</key>
|
||||||
@@ -223,22 +223,22 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
A5VM8GOD6pgO+5umTAfGePUVpKE=
|
M0Oo9dh3zLva6qpGJhBbgAxAIuY=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
GNOzk3bli3Um2gMNnpq6O6rl97hq0dZfj5BIgDSfl9A=
|
Nef5r8Bidx1FeTffn9+uzNZMlwxu1J37V4LOzOibxx4=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_hints.h</key>
|
<key>Headers/SDL_hints.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
2fcgomKzlDXR0nG2BTecGYNI2fw=
|
/SzqZQDEmBA9aPLBuEQJLeHNGv4=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
c8vMBfNzwpL6I0l2aghiLSNE3c/ZmOnQCIJCucACo7k=
|
17+Nv9lXJBOkimhfhtzZJcBqVKWf8z5tK/KQrvvU9xE=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_joystick.h</key>
|
<key>Headers/SDL_joystick.h</key>
|
||||||
@@ -267,11 +267,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
gMeCtdre9A0i6YsiBcA9UYyFU2M=
|
Fwnbxg/6DtHaQufmjH9Sx/zbF+A=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
7q8a96GCxzmM/IeuC5Qe6HdURJ/tkw7ZE+w1EB05MMU=
|
m69IH60aX73QRefpJOur+rWzKRZJH9RF7P4LNUxti+g=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_loadso.h</key>
|
<key>Headers/SDL_loadso.h</key>
|
||||||
@@ -285,6 +285,17 @@
|
|||||||
bpcu/Ms5CE5EHVV1zXIfWghw8JM8qB2114xfsIUBPeE=
|
bpcu/Ms5CE5EHVV1zXIfWghw8JM8qB2114xfsIUBPeE=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>Headers/SDL_locale.h</key>
|
||||||
|
<dict>
|
||||||
|
<key>hash</key>
|
||||||
|
<data>
|
||||||
|
JCF6SSXRcv6uAPICgwmTq+WsVZs=
|
||||||
|
</data>
|
||||||
|
<key>hash2</key>
|
||||||
|
<data>
|
||||||
|
SYFN/L2S2pKTNZQe4dGUKTPTFXbqXZlZ3Vyl4y5wZ8s=
|
||||||
|
</data>
|
||||||
|
</dict>
|
||||||
<key>Headers/SDL_log.h</key>
|
<key>Headers/SDL_log.h</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
@@ -454,11 +465,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
U6CRAlpSfGNDzBGXdbi4oftVkwU=
|
dk4R8c81l+WVpQ9ueWDS2SGuXkA=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
DgjII2f8X5emc/fKsW0hur9MGpTs8qpaTHe/NC7WdGE=
|
AQ9Th5IdVLO2XfkpMUuZAqxtYQXaQiiiHQVjTJyRq2g=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_platform.h</key>
|
<key>Headers/SDL_platform.h</key>
|
||||||
@@ -531,11 +542,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
DLm3FArKEgTtJ69RUmMmUvF0NsI=
|
FWcGz9/xlaBkrR1vdvYvDibFPhg=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
FIL6cT/Y+0AXFs3GF9EJ3m2pbkmMJxT/I+LuZP4/JrU=
|
OWI95ONgZvdn/FBJfMHV1BQWKQjx5LBN7MYuXwAadgg=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_scancode.h</key>
|
<key>Headers/SDL_scancode.h</key>
|
||||||
@@ -575,11 +586,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
smZhl9uPKfaFlYEpDbqiYDUGseY=
|
q9VYwFS+f9MaSiVwltDDrBpP8+o=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
h9ZnfoqEmhF2HFFwjE00RNDngk+Lg0Fth+XPTKM/OdA=
|
hfUwgCUrzO8oRZglsZlgHLqCVGo4tlorFxvRjh+d/A8=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_surface.h</key>
|
<key>Headers/SDL_surface.h</key>
|
||||||
@@ -597,11 +608,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
3tHnp+2bdRcZ3HH7EKLtOcbMlWQ=
|
iiUlVUMHJLVYtZYy8fesmrBw71U=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
ZwR9c/Bto10ts3gyJZgBeZEDkcfYBxbXqZYho1rDulI=
|
SmI4OODh0gdcy0WduqkDD+AGZI66vpEOgPEXc7JY3sM=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_syswm.h</key>
|
<key>Headers/SDL_syswm.h</key>
|
||||||
@@ -619,11 +630,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
3tGB/eQF+z1hhlWNiw4xuJ6c0NA=
|
bN9PvUNyNE5PSLQzjdF4b38bzRY=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
7WhLanB12mw0SlTjQFEZzDejcHmHtq/wBtc7oujsSSQ=
|
MymOftu/SUBMl4mrBetXv06hTmexfmEwWFuvGoMdmT8=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/SDL_timer.h</key>
|
<key>Headers/SDL_timer.h</key>
|
||||||
@@ -685,11 +696,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
0R0iBIIZHeiqfvyj+3+sbtepfwo=
|
Hd6qVU8OT67v88A0rMVhZpceGQg=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
ZUl2Z6rj4rI2+mitJQB2d+4hTIuTIzVf0fr7KM9hgKw=
|
rD48rqfdCs8UEGgpQilcPwZo50sH0NXKds0uYKXnkc8=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Headers/begin_code.h</key>
|
<key>Headers/begin_code.h</key>
|
||||||
@@ -718,11 +729,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
kQWsUbAzKB6cDjSyOGRQOM++OWk=
|
sh0oOLkdzcvMFVtkzp3NXS7VQh4=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
z/sm5HUSHetKQYB64C+hQcCeWrT1zzAW5tX83K65RQ0=
|
RWZqKrfBsPmghlDP7CT9Jf6NcUep+mHS5mvZnMIuieY=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Resources/License.txt</key>
|
<key>Resources/License.txt</key>
|
||||||
@@ -751,11 +762,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>hash</key>
|
<key>hash</key>
|
||||||
<data>
|
<data>
|
||||||
dF9gKJnv0WdQs57PRAzLo+EyGAA=
|
awST523PmC36+lehlmfUr/pwZko=
|
||||||
</data>
|
</data>
|
||||||
<key>hash2</key>
|
<key>hash2</key>
|
||||||
<data>
|
<data>
|
||||||
A5lFX3lFIAtu4Lxgjh7TXdIVDe0n6G1iPofA+3H+xMA=
|
+Foetj5EZqEAqjfkJcaUPc5XEgo/kcNG4bP0vzWuVHA=
|
||||||
</data>
|
</data>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule moonlight-common-c/moonlight-common-c updated: 247b1fe0e3...dbedf938d4
@@ -47,6 +47,7 @@ SOURCES += \
|
|||||||
$$COMMON_C_DIR/src/AudioStream.c \
|
$$COMMON_C_DIR/src/AudioStream.c \
|
||||||
$$COMMON_C_DIR/src/ByteBuffer.c \
|
$$COMMON_C_DIR/src/ByteBuffer.c \
|
||||||
$$COMMON_C_DIR/src/Connection.c \
|
$$COMMON_C_DIR/src/Connection.c \
|
||||||
|
$$COMMON_C_DIR/src/ConnectionTester.c \
|
||||||
$$COMMON_C_DIR/src/ControlStream.c \
|
$$COMMON_C_DIR/src/ControlStream.c \
|
||||||
$$COMMON_C_DIR/src/FakeCallbacks.c \
|
$$COMMON_C_DIR/src/FakeCallbacks.c \
|
||||||
$$COMMON_C_DIR/src/InputStream.c \
|
$$COMMON_C_DIR/src/InputStream.c \
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,7 @@ win32:!winrt {
|
|||||||
SUBDIRS += AntiHooking
|
SUBDIRS += AntiHooking
|
||||||
app.depends += AntiHooking
|
app.depends += AntiHooking
|
||||||
}
|
}
|
||||||
!winrt {
|
!winrt:win32|macx {
|
||||||
SUBDIRS += soundio
|
SUBDIRS += soundio
|
||||||
app.depends += soundio
|
app.depends += soundio
|
||||||
}
|
}
|
||||||
@@ -22,3 +22,4 @@ CONFIG += debug_and_release
|
|||||||
# Run our compile tests
|
# Run our compile tests
|
||||||
load(configure)
|
load(configure)
|
||||||
qtCompileTest(SL)
|
qtCompileTest(SL)
|
||||||
|
qtCompileTest(EGL)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ popd
|
|||||||
|
|
||||||
echo Compiling Moonlight in $BUILD_CONFIG configuration
|
echo Compiling Moonlight in $BUILD_CONFIG configuration
|
||||||
pushd $BUILD_FOLDER
|
pushd $BUILD_FOLDER
|
||||||
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
make -j$(sysctl -n hw.logicalcpu) $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
echo Saving dSYM file
|
echo Saving dSYM file
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ set ARCH=%2
|
|||||||
rem Convert to lower case for windeployqt
|
rem Convert to lower case for windeployqt
|
||||||
if /I "%BUILD_CONFIG%"=="debug" (
|
if /I "%BUILD_CONFIG%"=="debug" (
|
||||||
set BUILD_CONFIG=debug
|
set BUILD_CONFIG=debug
|
||||||
|
set WIX_MUMS=10
|
||||||
) else (
|
) else (
|
||||||
if /I "%BUILD_CONFIG%"=="release" (
|
if /I "%BUILD_CONFIG%"=="release" (
|
||||||
set BUILD_CONFIG=release
|
set BUILD_CONFIG=release
|
||||||
|
set WIX_MUMS=10
|
||||||
) else (
|
) else (
|
||||||
if /I "%BUILD_CONFIG%"=="signed-release" (
|
if /I "%BUILD_CONFIG%"=="signed-release" (
|
||||||
set BUILD_CONFIG=release
|
set BUILD_CONFIG=release
|
||||||
@@ -77,7 +79,7 @@ popd
|
|||||||
|
|
||||||
echo Compiling Moonlight in %BUILD_CONFIG% configuration
|
echo Compiling Moonlight in %BUILD_CONFIG% configuration
|
||||||
pushd %BUILD_FOLDER%
|
pushd %BUILD_FOLDER%
|
||||||
nmake %BUILD_CONFIG%
|
%SOURCE_ROOT%\scripts\jom.exe %BUILD_CONFIG%
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -23,6 +23,18 @@
|
|||||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallInitialize" />
|
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallInitialize" />
|
||||||
<MediaTemplate CompressionLevel="high" EmbedCab="yes" />
|
<MediaTemplate CompressionLevel="high" EmbedCab="yes" />
|
||||||
|
|
||||||
|
<Property Id="MPSSVC_START">
|
||||||
|
<RegistrySearch Id="MpsSvcStart"
|
||||||
|
Root="HKLM"
|
||||||
|
Key="System\CurrentControlSet\Services\MpsSvc"
|
||||||
|
Name="Start"
|
||||||
|
Type="raw" />
|
||||||
|
</Property>
|
||||||
|
|
||||||
|
<Condition Message="Setup cannot proceed because the Windows Firewall service has been improperly disabled or stopped. You must start the Windows Firewall service (MpsSvc) to continue. If you would like to disable Windows Firewall properly, use the Windows Firewall options in Control Panel.">
|
||||||
|
<![CDATA[Installed OR MPSSVC_START="#2"]]>
|
||||||
|
</Condition>
|
||||||
|
|
||||||
<?if $(var.Platform) = x64 ?>
|
<?if $(var.Platform) = x64 ?>
|
||||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||||
<?else ?>
|
<?else ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user