Compare commits

..
24 Commits
Author SHA1 Message Date
Cameron Gutman 866e2f1762 Version 1.0.1 2019-05-27 09:31:01 -07:00
Cameron Gutman 0a705d6752 Fix video on GFE 3.19 2019-05-27 02:24:55 -07:00
Cameron Gutman 4f74fd5354 Fix uninitialized variables and a race condition with CVDisplayLinkStart() 2019-05-21 09:09:16 -07:00
Cameron Gutman 3df9a5d01c Version 1.0.0 2019-05-21 00:14:12 -07:00
Cameron Gutman 2682829bd3 Fix Steam Link audio latency cap 2019-05-20 23:57:27 -07:00
Cameron Gutman 30cfa2607d Hide mouse cursor after streaming on Steam Link 2019-05-19 17:43:46 -07:00
Cameron Gutman 4467c2e6ad Update SDL_GameControllerDB 2019-05-19 13:46:05 -07:00
Cameron Gutman 2c0e8a0ddf Send WoL packet to 48002 and 48010 to workaround ISP blocking of 7 and 9 2019-05-19 13:36:05 -07:00
Cameron Gutman 4c17f32a2e Fix extra non-working gamepad appearing to the host 2019-05-19 13:24:06 -07:00
Cameron Gutman 674220087f Finish mouse emulation support with overlay and bugfixes 2019-05-19 13:10:42 -07:00
Cameron Gutman fc8d5d5799 Add gamepad mouse emulation support 2019-05-19 12:17:23 -07:00
Cameron Gutman 4e4f04c174 Focus on first item when a gamepad is connected 2019-05-19 11:08:23 -07:00
Cameron Gutman 65c21f3392 Improve gamepad navigation on settings page 2019-05-19 10:16:54 -07:00
Cameron Gutman 97fb30cdf1 Fix a few bugs in CVDisplayLink integration in VTRenderer 2019-05-19 09:52:59 -07:00
Cameron Gutman c975279589 Only enable exception-based thread naming on debug builds 2019-05-13 17:54:03 -07:00
Cameron Gutman b3ee7a635f Cap queued audio at 40 ms on Steam Link 2019-05-11 19:09:59 -07:00
Cameron Gutman c2b12868bb Move DisplayLinkVsyncSource back into VTRenderer to reduce latency 2019-05-11 18:33:12 -07:00
Cameron Gutman bdbb03e16f Request 20 ms audio frames on Steam Link to reduce CPU overhead 2019-05-04 15:46:11 -07:00
Cameron Gutman 53138d7c16 Name threads for easier debugging 2019-05-03 21:18:58 -07:00
Cameron Gutman 18d1d35104 Use QByteArray::reserve() rather than reallocating each time we must resize 2019-05-02 22:54:18 -07:00
Cameron Gutman 99d9de35de Add NV21 pixel format support to SDL renderer 2019-05-02 22:51:28 -07:00
Cameron Gutman e6a48481a5 Restore the bulk submission optimization for Steam Link 2019-05-01 22:31:52 -07:00
Cameron Gutman 21f2b1224a Decode directly into the audio renderer's buffer to avoid a copy 2019-05-01 21:27:41 -07:00
Cameron Gutman 187f47a353 Update common-c to fix receive time corruption 2019-04-30 23:23:02 -07:00
35 changed files with 512 additions and 294 deletions

No files matched your search

+2 -4
View File
@@ -261,12 +261,10 @@ macx {
message(VideoToolbox renderer selected) message(VideoToolbox renderer selected)
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm \ streaming/video/ffmpeg-renderers/vt.mm
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \ HEADERS += \
streaming/video/ffmpeg-renderers/vt.h \ streaming/video/ffmpeg-renderers/vt.h
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
} }
soundio { soundio {
message(libsoundio audio renderer selected) message(libsoundio audio renderer selected)
+1 -1
View File
@@ -156,7 +156,7 @@ bool NvComputer::wake()
const quint16 WOL_PORTS[] = { const quint16 WOL_PORTS[] = {
7, 9, // Standard WOL ports 7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE 47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
}; };
// Create the WoL payload // Create the WoL payload
@@ -33,6 +33,33 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="1.0.1" date="2019-05-27">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed broken video on GeForce Experience 3.19</li>
</ul>
</description>
</release>
<release version="1.0.0" date="2019-05-21">
<description>
<p>New features:</p>
<ul>
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
<li>Improved gamepad UI navigation</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed duplicate non-working gamepads appearing on the host</li>
<li>Fixed excessive frame queue delay on macOS</li>
<li>Fixed excessive audio latency on Steam Link</li>
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="0.10.1" date="2019-04-28"> <release version="0.10.1" date="2019-04-28">
<description> <description>
<p>New features:</p> <p>New features:</p>
+6
View File
@@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
import AppModel 1.0 import AppModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView { CenteredGridView {
property int computerIndex property int computerIndex
@@ -32,6 +33,11 @@ CenteredGridView {
StackView.onActivated: { StackView.onActivated: {
appModel.computerLost.connect(computerLost) appModel.computerLost.connect(computerLost)
activated = true activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
} }
StackView.onDeactivating: { StackView.onDeactivating: {
+11
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
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
@@ -28,4 +30,13 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth) textWidth = Math.max(popupMetrics.width, textWidth)
} }
} }
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
} }
+11
View File
@@ -5,6 +5,7 @@ import ComputerModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView { CenteredGridView {
property ComputerModel computerModel : createModel() property ComputerModel computerModel : createModel()
@@ -27,6 +28,16 @@ CenteredGridView {
StackView.onActivated: { StackView.onActivated: {
// Setup signals on CM // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete) ComputerManager.computerAddCompleted.connect(addComplete)
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
} }
StackView.onDeactivating: { StackView.onDeactivating: {
+9 -2
View File
@@ -25,11 +25,18 @@ Flickable {
} }
StackView.onActivated: { StackView.onActivated: {
SdlGamepadKeyNavigation.setSettingsMode(true) // This enables Tab and BackTab based navigation rather than arrow keys.
// It is required to shift focus between controls on the settings page.
SdlGamepadKeyNavigation.setUiNavMode(true)
// Highlight the first item if a gamepad is connected
if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
resolutionComboBox.forceActiveFocus(Qt.TabFocus)
}
} }
StackView.onDeactivating: { StackView.onDeactivating: {
SdlGamepadKeyNavigation.setSettingsMode(false) SdlGamepadKeyNavigation.setUiNavMode(false)
// Save the prefs so the Session can observe the changes // Save the prefs so the Session can observe the changes
StreamingPreferences.save() StreamingPreferences.save()
-4
View File
@@ -19,10 +19,6 @@ ApplicationWindow {
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized" visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
Component.onCompleted: {
SdlGamepadKeyNavigation.enable()
}
StackView { StackView {
id: stackView id: stackView
initialItem: initialView initialItem: initialView
+8 -36
View File
@@ -10,7 +10,7 @@
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation() SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false), : m_Enabled(false),
m_SettingsMode(false), m_UiNavMode(false),
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
@@ -106,7 +106,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
switch (event.cbutton.button) { switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP: case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_SettingsMode) { if (m_UiNavMode) {
// Back-tab // Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
} }
@@ -115,7 +115,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
} }
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_SettingsMode) { if (m_UiNavMode) {
sendKey(type, Qt::Key_Tab); sendKey(type, Qt::Key_Tab);
} }
else { else {
@@ -124,24 +124,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left); sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right); sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break; break;
case SDL_CONTROLLER_BUTTON_A: case SDL_CONTROLLER_BUTTON_A:
if (m_SettingsMode) { if (m_UiNavMode) {
sendKey(type, Qt::Key_Space); sendKey(type, Qt::Key_Space);
} }
else { else {
@@ -183,7 +171,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing // Do nothing
} }
else if (leftY < -30000) { else if (leftY < -30000) {
if (m_SettingsMode) { if (m_UiNavMode) {
// Back-tab // Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -196,7 +184,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftY > 30000) { else if (leftY > 30000) {
if (m_SettingsMode) { if (m_UiNavMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
} }
@@ -210,27 +198,11 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) { else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left); sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left); sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftX > 30000) { else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right); sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right); sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
} }
@@ -246,9 +218,9 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
} }
} }
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode) void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{ {
m_SettingsMode = settingsMode; m_UiNavMode = uiNavMode;
} }
int SdlGamepadKeyNavigation::getConnectedGamepads() int SdlGamepadKeyNavigation::getConnectedGamepads()
+2 -2
View File
@@ -18,7 +18,7 @@ public:
Q_INVOKABLE void disable(); Q_INVOKABLE void disable();
Q_INVOKABLE void setSettingsMode(bool settingsMode); Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads(); Q_INVOKABLE int getConnectedGamepads();
@@ -32,6 +32,6 @@ private:
QTimer* m_PollingTimer; QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads; QList<SDL_GameController*> m_Gamepads;
bool m_Enabled; bool m_Enabled;
bool m_SettingsMode; bool m_UiNavMode;
Uint32 m_LastAxisNavigationEventTime; Uint32 m_LastAxisNavigationEventTime;
}; };
+7
View File
@@ -344,6 +344,13 @@ int main(int argc, char *argv[])
// Disable minimize on focus loss by default. Users seem to want this off by default. // Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
// After the QGuiApplication is created, the platform stuff will be initialized // After the QGuiApplication is created, the platform stuff will be initialized
+22 -12
View File
@@ -41,6 +41,7 @@ bool Session::testAudio(int audioConfiguration)
// the renderer the channel count and sample rate. // the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {}; OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000; opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
switch (audioConfiguration) switch (audioConfiguration)
{ {
@@ -143,25 +144,34 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
s_ActiveSession->m_AudioSampleCount++; s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) { if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder, samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData, (unsigned char*)sampleData,
sampleLength, sampleLength,
s_ActiveSession->m_OpusDecodeBuffer, (short*)buffer,
SAMPLES_PER_FRAME, desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
0); 0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) { if (samplesDecoded > 0) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer, SDL_assert(desiredSize >= sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount);
static_cast<int>( desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
sizeof(short) * }
samplesDecoded * else {
s_ActiveSession->m_AudioConfig.channelCount))) { desiredSize = 0;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
"Reinitializing audio renderer after failure"); SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer; delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr; s_ActiveSession->m_AudioRenderer = nullptr;
}
} }
} }
+3 -4
View File
@@ -2,9 +2,6 @@
#include <Limelight.h> #include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer class IAudioRenderer
{ {
public: public:
@@ -12,6 +9,8 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0; virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized // Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0; virtual bool submitAudio(int bytesWritten) = 0;
}; };
+4 -1
View File
@@ -12,8 +12,11 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize); virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
private: private:
SDL_AudioDeviceID m_AudioDevice; SDL_AudioDeviceID m_AudioDevice;
void* m_AudioBuffer;
}; };
+26 -5
View File
@@ -6,7 +6,8 @@
#include <QtGlobal> #include <QtGlobal>
SdlAudioRenderer::SdlAudioRenderer() SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0) : m_AudioDevice(0),
m_AudioBuffer(nullptr)
{ {
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
@@ -30,14 +31,20 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
// frames contain a non-power of 2 number of samples, // frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame. // so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms. // Specifying non-Po2 seems to work for our supported platforms.
want.samples = SAMPLES_PER_FRAME; want.samples = opusConfig->samplesPerFrame;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) { if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s", "Failed to open audio device: %s",
SDL_GetError()); SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO); return false;
}
m_AudioBuffer = malloc(opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount);
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate audio buffer");
return false; return false;
} }
@@ -65,13 +72,27 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_CloseAudioDevice(m_AudioDevice); SDL_CloseAudioDevice(m_AudioDevice);
} }
if (m_AudioBuffer != nullptr) {
free(m_AudioBuffer);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
} }
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize) void* SdlAudioRenderer::getAudioBuffer(int*)
{ {
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) { return m_AudioBuffer;
}
bool SdlAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// Nothing to do
return true;
}
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s", "Failed to queue audio sample: %s",
SDL_GetError()); SDL_GetError());
+33 -30
View File
@@ -2,16 +2,10 @@
#include <SDL.h> #include <SDL.h>
// To reduce CPU load on the Steam Link, we need to accumulate several frames
// before submitting for playback. Higher frames per submission saves more CPU
// but increases audio latency.
#define FRAMES_PER_SUBMISSION 4
SLAudioRenderer::SLAudioRenderer() SLAudioRenderer::SLAudioRenderer()
: m_AudioContext(nullptr), : m_AudioContext(nullptr),
m_AudioStream(nullptr), m_AudioStream(nullptr),
m_AudioBuffer(nullptr), m_AudioBuffer(nullptr)
m_AudioBufferBytesFilled(0)
{ {
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr); SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
} }
@@ -25,7 +19,12 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
return false; return false;
} }
m_AudioBufferSize = SAMPLES_PER_FRAME * sizeof(short) * opusConfig->channelCount * FRAMES_PER_SUBMISSION; // This number is pretty conservative (especially for surround), but
// it's hard to avoid since we get crushed by CPU limitations.
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
m_FrameDuration = opusConfig->samplesPerFrame / 48;
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioStream = SLAudio_CreateStream(m_AudioContext, m_AudioStream = SLAudio_CreateStream(m_AudioContext,
opusConfig->sampleRate, opusConfig->sampleRate,
opusConfig->channelCount, opusConfig->channelCount,
@@ -38,17 +37,26 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
} }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SLAudio renderer"); "Using SLAudio renderer with %d ms frames",
m_FrameDuration);
return true; return true;
} }
void* SLAudioRenderer::getAudioBuffer(int* size)
{
SDL_assert(*size == m_AudioBufferSize);
if (m_AudioBuffer == nullptr) {
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
}
return m_AudioBuffer;
}
SLAudioRenderer::~SLAudioRenderer() SLAudioRenderer::~SLAudioRenderer()
{ {
if (m_AudioBufferBytesFilled != 0) { if (m_AudioBuffer != nullptr) {
// We had a buffer in flight when we quit. Just in case
// SLAudio doesn't handle this properly, we'll zero and submit
// it just to be safe.
memset(m_AudioBuffer, 0, m_AudioBufferSize); memset(m_AudioBuffer, 0, m_AudioBufferSize);
SLAudio_SubmitFrame(m_AudioStream); SLAudio_SubmitFrame(m_AudioStream);
} }
@@ -62,32 +70,27 @@ SLAudioRenderer::~SLAudioRenderer()
} }
} }
bool SLAudioRenderer::submitAudio(short* audioBuffer, int audioSize) bool SLAudioRenderer::submitAudio(int bytesWritten)
{ {
if (m_AudioBufferBytesFilled == 0) { if (bytesWritten == 0) {
// Get a new audio buffer from SLAudio // This buffer will be reused next time
m_AudioBuffer = (char*)SLAudio_BeginFrame(m_AudioStream); return true;
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SLAudio_BeginFrame() failed");
return true;
}
} }
// Accumulate several frames of audio before submitting to reduce CPU load if (LiGetPendingAudioFrames() * m_FrameDuration < m_MaxQueuedAudioMs) {
SDL_assert(audioSize <= m_AudioBufferSize - m_AudioBufferBytesFilled);
memcpy(&m_AudioBuffer[m_AudioBufferBytesFilled], audioBuffer, audioSize);
m_AudioBufferBytesFilled += audioSize;
// Submit the buffer when it's full
if (m_AudioBufferBytesFilled == m_AudioBufferSize) {
SLAudio_SubmitFrame(m_AudioStream); SLAudio_SubmitFrame(m_AudioStream);
m_AudioBufferBytesFilled = 0; m_AudioBuffer = nullptr;
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Too many queued audio frames: %d",
LiGetPendingAudioFrames());
} }
return true; return true;
} }
void SLAudioRenderer::slLogCallback(void *context, ESLAudioLog logLevel, const char *message) void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
{ {
SDL_LogPriority priority; SDL_LogPriority priority;
+7 -3
View File
@@ -12,14 +12,18 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize); virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
private: private:
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message); static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
CSLAudioContext* m_AudioContext; CSLAudioContext* m_AudioContext;
CSLAudioStream* m_AudioStream; CSLAudioStream* m_AudioStream;
char* m_AudioBuffer;
void* m_AudioBuffer;
int m_AudioBufferSize; int m_AudioBufferSize;
int m_AudioBufferBytesFilled; int m_FrameDuration;
int m_MaxQueuedAudioMs;
}; };
@@ -260,7 +260,7 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo, m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample * m_OutputStream->bytes_per_sample *
m_OpusChannelCount * m_OpusChannelCount *
SAMPLES_PER_FRAME * opusConfig->samplesPerFrame *
packetsToBuffer); packetsToBuffer);
if (m_RingBuffer == nullptr) { if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -284,15 +284,8 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return true; return true;
} }
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize) void* SoundIoAudioRenderer::getAudioBuffer(int* size)
{ {
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// We must always write a full frame of audio. If we don't, // We must always write a full frame of audio. If we don't,
// the reader will get out of sync with the writer and our // the reader will get out of sync with the writer and our
// channels will get all mixed up. To ensure this is always // channels will get all mixed up. To ensure this is always
@@ -300,9 +293,26 @@ bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
// of our frame size. // of our frame size.
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer); int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample; int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame); *size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame);
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite); return soundio_ring_buffer_write_ptr(m_RingBuffer);
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite); }
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
{
if (m_Errored) {
return false;
}
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// Advance the write pointer
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
return true; return true;
} }
@@ -13,7 +13,9 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual bool submitAudio(short* audioBuffer, int audioSize); virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
private: private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
+147 -2
View File
@@ -5,6 +5,7 @@
#include "path.h" #include "path.h"
#include <QtGlobal> #include <QtGlobal>
#include <QtMath>
#include <QDir> #include <QDir>
#define VK_0 0x30 #define VK_0 0x30
@@ -28,6 +29,18 @@
// How far the finger can move before it cancels a drag or tap // How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.1f #define DEAD_ZONE_DELTA 0.1f
// 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
const int SdlInputHandler::k_ButtonMap[] = { const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG, A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG, BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
@@ -81,6 +94,12 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
MappingManager mappingManager; MappingManager mappingManager;
mappingManager.applyMappings(); 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 // We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init. // an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER)); SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
@@ -115,6 +134,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
SdlInputHandler::~SdlInputHandler() SdlInputHandler::~SdlInputHandler()
{ {
for (int i = 0; i < MAX_GAMEPADS; i++) { 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 (m_GamepadState[i].haptic != nullptr) { if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic); SDL_HapticClose(m_GamepadState[i].haptic);
} }
@@ -139,6 +162,14 @@ SdlInputHandler::~SdlInputHandler()
// Return background event handling to off // Return background event handling to off
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0"); 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) void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
@@ -658,6 +689,41 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
return interval; 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) void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{ {
SDL_JoystickID gameControllerId = event->which; SDL_JoystickID gameControllerId = event->which;
@@ -716,7 +782,10 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION); SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
} }
sendGamepadState(state); // 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) void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
@@ -728,9 +797,76 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
if (event->state == SDL_PRESSED) { if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button]; 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 { else {
state->buttons &= ~k_ButtonMap[event->button]; 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 {
// 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 // Handle Start+Select+L1+R1 as a gamepad quit combo
@@ -750,7 +886,10 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
return; return;
} }
sendGamepadState(state); // 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) void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
@@ -780,6 +919,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
i = 0; i = 0;
for (; i < MAX_GAMEPADS; i++) { for (; i < MAX_GAMEPADS; i++) {
SDL_assert(m_GamepadState[i].controller != controller);
if (m_GamepadState[i].controller == NULL) { if (m_GamepadState[i].controller == NULL) {
// Found an empty slot // Found an empty slot
break; break;
@@ -855,6 +995,11 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which); state = findStateForGamepad(event->which);
if (state != NULL) { if (state != NULL) {
if (state->mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(state->mouseEmulationTimer);
}
SDL_GameControllerClose(state->controller); SDL_GameControllerClose(state->controller);
if (state->haptic != nullptr) { if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic); SDL_HapticClose(state->haptic);
+6
View File
@@ -13,6 +13,9 @@ struct GamepadState {
int hapticEffectId; int hapticEffectId;
short index; short index;
SDL_TimerID mouseEmulationTimer;
uint32_t lastStartDownTime;
short buttons; short buttons;
short lsX, lsY; short lsX, lsY;
short rsX, rsY; short rsX, rsY;
@@ -82,6 +85,9 @@ private:
static static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param); Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
bool m_MultiController; bool m_MultiController;
SDL_TimerID m_MouseMoveTimer; SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX; SDL_atomic_t m_MouseDeltaX;
+28 -4
View File
@@ -49,11 +49,13 @@ AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
nullptr, nullptr,
Session::arCleanup, Session::arCleanup,
Session::arDecodeAndPlaySample, Session::arDecodeAndPlaySample,
CAPABILITY_DIRECT_SUBMIT | #ifndef STEAM_LINK
#ifdef STEAM_LINK CAPABILITY_DIRECT_SUBMIT
CAPABILITY_SLOW_OPUS_DECODER
#else #else
0 // We cannot use direct submit for Steam Link because the
// SLAudio renderer needs to look at the audio backlog to
// cap latency since playback is a blocking operation.
CAPABILITY_SLOW_OPUS_DECODER
#endif #endif
}; };
@@ -130,6 +132,11 @@ void Session::clConnectionStatusUpdate(int connectionStatus)
return; return;
} }
if (s_ActiveSession->m_MouseEmulationRefCount > 0) {
// Don't display the overlay if mouse emulation is already using it
return;
}
switch (connectionStatus) switch (connectionStatus)
{ {
case CONN_STATUS_POOR: case CONN_STATUS_POOR:
@@ -307,6 +314,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr), m_InputHandler(nullptr),
m_InputHandlerLock(0), m_InputHandlerLock(0),
m_MouseEmulationRefCount(0),
m_OpusDecoder(nullptr), m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr), m_AudioRenderer(nullptr),
m_AudioSampleCount(0), m_AudioSampleCount(0),
@@ -825,6 +833,22 @@ void Session::toggleFullscreen()
} }
} }
void Session::notifyMouseEmulationMode(bool enabled)
{
m_MouseEmulationRefCount += enabled ? 1 : -1;
SDL_assert(m_MouseEmulationRefCount >= 0);
// We re-use the status update overlay for mouse mode notification
if (m_MouseEmulationRefCount > 0) {
strcpy(m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Gamepad mouse mode active\nLong press Start to deactivate");
m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
}
else {
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
}
}
void Session::exec(int displayOriginX, int displayOriginY) void Session::exec(int displayOriginX, int displayOriginY)
{ {
m_DisplayOriginX = displayOriginX; m_DisplayOriginX = displayOriginX;
+3 -1
View File
@@ -77,6 +77,8 @@ private:
void toggleFullscreen(); void toggleFullscreen();
void notifyMouseEmulationMode(bool enabled);
void updateOptimalWindowDisplayMode(); void updateOptimalWindowDisplayMode();
static static
@@ -141,6 +143,7 @@ private:
bool m_UnexpectedTermination; bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler; SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock; SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;
@@ -148,7 +151,6 @@ private:
int m_ActiveVideoFrameRate; int m_ActiveVideoFrameRate;
OpusMSDecoder* m_OpusDecoder; OpusMSDecoder* m_OpusDecoder;
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer; IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig; OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount; int m_AudioSampleCount;
@@ -1,11 +0,0 @@
#pragma once
#include "pacer.h"
class DisplayLinkVsyncSourceFactory
{
public:
static
IVsyncSource* createVsyncSource(Pacer* pacer);
};
@@ -1,132 +0,0 @@
#include "displaylinkvsyncsource.h"
#include <SDL_syswm.h>
#include <CoreVideo/CoreVideo.h>
#import <Cocoa/Cocoa.h>
class DisplayLinkVsyncSource : public IVsyncSource
{
public:
DisplayLinkVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_DisplayLink(nullptr)
{
}
virtual ~DisplayLinkVsyncSource() override
{
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
}
static
CGDirectDisplayID
getDisplayID(NSScreen* screen)
{
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
return [screenNumber unsignedIntValue];
}
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
// In my testing on macOS 10.13, this callback is invoked about 24 ms
// prior to the specified v-sync time (now - vsyncTime). Since this is
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
// draw using the current host time, rather than the actual v-sync target
// time. Because the CVDisplayLink is in sync with the actual v-sync
// interval, even if many ms prior, we can safely use the current host time
// and get a consistent callback for each v-sync. This reduces video latency
// by at least 1 frame vs. rendering with the actual vsyncTime.
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
return kCVReturnSuccess;
}
virtual bool initialize(SDL_Window* window, int displayFps) override
{
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
m_DisplayFps = displayFps;
NSScreen* screen = [info.info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId;
displayId = getDisplayID(screen);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
private:
Pacer* m_Pacer;
CVDisplayLinkRef m_DisplayLink;
int m_DisplayFps;
};
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
return new DisplayLinkVsyncSource(pacer);
}
@@ -61,7 +61,7 @@ bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
m_Window = info.info.win.window; m_Window = info.info.win.window;
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this); m_Thread = SDL_CreateThread(vsyncThread, "DXVsync", this);
if (m_Thread == nullptr) { if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s", "Unable to create DX V-sync thread: %s",
@@ -18,7 +18,7 @@ NullThreadedVsyncSource::~NullThreadedVsyncSource()
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps) bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
{ {
m_DisplayFps = displayFps; m_DisplayFps = displayFps;
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this); m_Thread = SDL_CreateThread(vsyncThread, "NullVsync", this);
if (m_Thread == nullptr) { if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s", "Unable to create DX V-sync thread: %s",
@@ -3,10 +3,6 @@
#include "nullthreadedvsyncsource.h" #include "nullthreadedvsyncsource.h"
#ifdef Q_OS_DARWIN
#include "displaylinkvsyncsource.h"
#endif
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <Windows.h> #include <Windows.h>
@@ -230,9 +226,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
"Frame pacing active: target %d Hz with %d FPS stream", "Frame pacing active: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
#if defined(Q_OS_DARWIN) #if defined(Q_OS_WIN32)
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
#elif defined(Q_OS_WIN32)
// Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because // Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because
// it blocks during other concurrent DX operations (like actually rendering). // it blocks during other concurrent DX operations (like actually rendering).
if (IsWindows8OrGreater()) { if (IsWindows8OrGreater()) {
@@ -254,7 +248,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
} }
if (m_VsyncRenderer->isRenderThreadSupported()) { if (m_VsyncRenderer->isRenderThreadSupported()) {
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "Pacer Render Thread", this); m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this);
} }
return true; return true;
@@ -9,7 +9,8 @@
const std::vector<int> SdlRenderer::k_SwFormats({ const std::vector<int> SdlRenderer::k_SwFormats({
AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P,
AV_PIX_FMT_NV12 AV_PIX_FMT_NV12,
AV_PIX_FMT_NV21
}); });
SdlRenderer::SdlRenderer() SdlRenderer::SdlRenderer()
@@ -285,6 +286,9 @@ void SdlRenderer::renderFrame(AVFrame* frame)
case AV_PIX_FMT_NV12: case AV_PIX_FMT_NV12:
sdlFormat = SDL_PIXELFORMAT_NV12; sdlFormat = SDL_PIXELFORMAT_NV12;
break; break;
case AV_PIX_FMT_NV21:
sdlFormat = SDL_PIXELFORMAT_NV21;
break;
default: default:
SDL_assert(false); SDL_assert(false);
goto Exit; goto Exit;
+108 -9
View File
@@ -22,13 +22,29 @@ public:
: m_HwContext(nullptr), : m_HwContext(nullptr),
m_DisplayLayer(nullptr), m_DisplayLayer(nullptr),
m_FormatDesc(nullptr), m_FormatDesc(nullptr),
m_StreamView(nullptr) m_StreamView(nullptr),
m_DisplayLink(nullptr),
m_VsyncMutex(nullptr),
m_VsyncPassed(nullptr)
{ {
SDL_zero(m_OverlayTextFields); SDL_zero(m_OverlayTextFields);
} }
virtual ~VTRenderer() override virtual ~VTRenderer() override
{ {
if (m_DisplayLink != nullptr) {
CVDisplayLinkStop(m_DisplayLink);
CVDisplayLinkRelease(m_DisplayLink);
}
if (m_VsyncPassed != nullptr) {
SDL_DestroyCond(m_VsyncPassed);
}
if (m_VsyncMutex != nullptr) {
SDL_DestroyMutex(m_VsyncMutex);
}
if (m_HwContext != nullptr) { if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext); av_buffer_unref(&m_HwContext);
} }
@@ -48,6 +64,78 @@ public:
} }
} }
static
CVReturn
displayLinkOutputCallback(
CVDisplayLinkRef displayLink,
const CVTimeStamp* /* now */,
const CVTimeStamp* /* vsyncTime */,
CVOptionFlags,
CVOptionFlags*,
void *displayLinkContext)
{
auto me = reinterpret_cast<VTRenderer*>(displayLinkContext);
SDL_assert(displayLink == me->m_DisplayLink);
SDL_LockMutex(me->m_VsyncMutex);
SDL_CondSignal(me->m_VsyncPassed);
SDL_UnlockMutex(me->m_VsyncMutex);
return kCVReturnSuccess;
}
bool initializeVsyncCallback(SDL_SysWMinfo* info)
{
NSScreen* screen = [info->info.cocoa.window screen];
CVReturn status;
if (screen == nullptr) {
// Window not visible on any display, so use a
// CVDisplayLink that can work with all active displays.
// When we become visible, we'll recreate ourselves
// and associate with the new screen.
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow is not visible on any display");
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
}
else {
CGDirectDisplayID displayId = [[screen deviceDescription][@"NSScreenNumber"] unsignedIntValue];
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"NSWindow on display: %x",
displayId);
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
}
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create CVDisplayLink: %d",
status);
return false;
}
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkSetOutputCallback() failed: %d",
status);
return false;
}
// The CVDisplayLink callback uses these, so we must initialize them before
// starting the callbacks.
m_VsyncMutex = SDL_CreateMutex();
m_VsyncPassed = SDL_CreateCond();
status = CVDisplayLinkStart(m_DisplayLink);
if (status != kCVReturnSuccess) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CVDisplayLinkStart() failed: %d",
status);
return false;
}
return true;
}
// Caller frees frame after we return // Caller frees frame after we return
virtual void renderFrame(AVFrame* frame) override virtual void renderFrame(AVFrame* frame) override
{ {
@@ -104,6 +192,16 @@ public:
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer]; [m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer); CFRelease(sampleBuffer);
if (m_DisplayLink != nullptr) {
// Vsync is enabled, so wait for a swap before returning
SDL_LockMutex(m_VsyncMutex);
if (SDL_CondWaitTimeout(m_VsyncPassed, m_VsyncMutex, 100) == SDL_MUTEX_TIMEDOUT) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"V-sync wait timed out after 100 ms");
}
SDL_UnlockMutex(m_VsyncMutex);
}
} }
virtual bool initialize(PDECODER_PARAMETERS params) override virtual bool initialize(PDECODER_PARAMETERS params) override
@@ -191,6 +289,12 @@ public:
return false; return false;
} }
if (params->enableVsync) {
if (!initializeVsyncCallback(&info)) {
return false;
}
}
return true; return true;
} }
@@ -278,20 +382,15 @@ public:
return true; return true;
} }
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
{
// This renderer is inherently tied to V-sync due how we're
// rendering with AVSampleBufferDisplay layer. Running without
// the V-Sync source leads to massive stuttering.
return PACING_FORCE_ON;
}
private: private:
AVBufferRef* m_HwContext; AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer; AVSampleBufferDisplayLayer* m_DisplayLayer;
CMVideoFormatDescriptionRef m_FormatDesc; CMVideoFormatDescriptionRef m_FormatDesc;
NSView* m_StreamView; NSView* m_StreamView;
NSTextField* m_OverlayTextFields[Overlay::OverlayMax]; NSTextField* m_OverlayTextFields[Overlay::OverlayMax];
CVDisplayLinkRef m_DisplayLink;
SDL_mutex* m_VsyncMutex;
SDL_cond* m_VsyncPassed;
}; };
IFFmpegRenderer* VTRendererFactory::createRenderer() { IFFmpegRenderer* VTRendererFactory::createRenderer() {
+3 -3
View File
@@ -620,9 +620,9 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Add some extra space in case we need to do an SPS fixup // Add some extra space in case we need to do an SPS fixup
requiredBufferSize += MAX_SPS_EXTRA_SIZE; requiredBufferSize += MAX_SPS_EXTRA_SIZE;
} }
if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0); // Ensure the decoder buffer is large enough
} m_DecodeBuffer.reserve(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE);
int offset = 0; int offset = 0;
while (entry != nullptr) { while (entry != nullptr) {
+1 -1
View File
@@ -109,7 +109,7 @@ SLVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
return DR_OK; return DR_OK;
} }
void SLVideoDecoder::slLogCallback(void *context, ESLVideoLog logLevel, const char *message) void SLVideoDecoder::slLogCallback(void*, ESLVideoLog logLevel, const char *message)
{ {
SDL_LogPriority priority; SDL_LogPriority priority;
+1 -1
View File
@@ -1 +1 @@
0.10.1 1.0.1