From f018a71a8a2a63377acde15366625e8effeae05b Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 17 Oct 2026 14:06:10 -0500 Subject: [PATCH 1/5] Override NSApplicationUpdateCycleEnabled to fix reduced mouse polling rate Apps built with the macOS 26+ SDK default to an event loop synchronized to the display which caps input polling rates. We have to use the plist for the opt-out, since NSApp is already set up by Qt before SDL can set this default. Fixes #2036 --- app/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Info.plist b/app/Info.plist index 24980b7757..cfee9bcea2 100644 --- a/app/Info.plist +++ b/app/Info.plist @@ -8,6 +8,8 @@ Moonlight LSSupportsGameMode + NSApplicationUpdateCycleEnabled + CFBundleGetInfoString Stream games and other applications from another PC CFBundleIconFile From 9b8886a6f578b1011094984040609ef053442c92 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 18 Oct 2026 23:21:03 -0500 Subject: [PATCH 2/5] Fix integer truncation warning on MSVC --- h264bitstream/bs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/h264bitstream/bs.h b/h264bitstream/bs.h index fc8fbf4462..fad69b0860 100644 --- a/h264bitstream/bs.h +++ b/h264bitstream/bs.h @@ -101,7 +101,7 @@ static inline int bs_eof(bs_t* b) { if (b->p >= b->end) { return 1; } else { ret static inline int bs_overrun(bs_t* b) { if (b->p > b->end) { return 1; } else { return 0; } } -static inline int bs_pos(bs_t* b) { if (b->p > b->end) { return (b->end - b->start); } else { return (b->p - b->start); } } +static inline int bs_pos(bs_t* b) { if (b->p > b->end) { return (int)(b->end - b->start); } else { return (int)(b->p - b->start); } } static inline uint32_t bs_read_u1(bs_t* b) { @@ -217,7 +217,7 @@ static inline void bs_write_u8(bs_t* b, uint32_t v) #ifdef FAST_U8 if (b->bits_left == 8 && ! bs_eof(b)) // can do fast write { - b->p[0] = v; + b->p[0] = (uint8_t)v; b->p++; return; } From e4db5c2c18e0a55fd8a33d8d735d8b71c1f9a710 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 19 Oct 2026 00:18:55 -0500 Subject: [PATCH 3/5] Check for pacing queue emptiness before dequeuing This shouldn't actually be reachable when the queue is empty, but we can check anyway to be safe. Ref: #2054 --- app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp index 27fec6561c..4ad1477829 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp @@ -249,7 +249,7 @@ void Pacer::handleVsync(int timeUntilNextVsyncMillis) return; } - if (m_Stopping) { + if (m_Stopping || m_PacingQueue.isEmpty()) { m_FrameQueueLock.unlock(); return; } From 9af0e9f53995949cf71a5ef9ed76eb99cb086b78 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 20 Oct 2026 00:29:25 -0500 Subject: [PATCH 4/5] Improve robustness and error handling of DXVsyncSource --- .../ffmpeg-renderers/pacer/dxvsyncsource.cpp | 67 ++++++++++++------- .../ffmpeg-renderers/pacer/dxvsyncsource.h | 4 +- .../video/ffmpeg-renderers/pacer/pacer.cpp | 50 ++++++++------ .../video/ffmpeg-renderers/pacer/pacer.h | 3 +- 4 files changed, 77 insertions(+), 47 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp index 2d30aadb81..dbfb0a398a 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp @@ -10,6 +10,8 @@ DxVsyncSource::DxVsyncSource(Pacer* pacer) : m_LastMonitor(nullptr) { SDL_zero(m_WaitForVblankEventParams); + m_WaitForVblankEventParams.VidPnSourceId = D3DDDI_ID_UNINITIALIZED; + SDL_zero(m_AdapterLuid); } DxVsyncSource::~DxVsyncSource() @@ -72,41 +74,43 @@ bool DxVsyncSource::isAsync() return false; } -void DxVsyncSource::waitForVsync() +bool DxVsyncSource::waitForVsync() { NTSTATUS status; // If the monitor has changed from last time, open the new adapter HMONITOR currentMonitor = MonitorFromWindow(m_Window, MONITOR_DEFAULTTONEAREST); - if (currentMonitor != m_LastMonitor) { + if (!currentMonitor || currentMonitor != m_LastMonitor) { + // Close the old adapter + if (m_WaitForVblankEventParams.hAdapter != 0) { + D3DKMT_CLOSEADAPTER closeAdapterParams = {}; + closeAdapterParams.hAdapter = m_WaitForVblankEventParams.hAdapter; + m_D3DKMTCloseAdapter(&closeAdapterParams); + m_WaitForVblankEventParams.hAdapter = 0; + } + m_LastMonitor = nullptr; + + // We can't wait if we're not on any monitor + if (!currentMonitor) { + return false; + } + MONITORINFOEXA monitorInfo = {}; monitorInfo.cbSize = sizeof(monitorInfo); if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GetMonitorInfo() failed: %d", GetLastError()); - return; + return false; } - DEVMODEA monitorMode; + DEVMODEA monitorMode = {}; monitorMode.dmSize = sizeof(monitorMode); if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "EnumDisplaySettings() failed: %d", GetLastError()); - return; - } - - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, - "Monitor changed: %s %d Hz", - monitorInfo.szDevice, - monitorMode.dmDisplayFrequency); - - // Close the old adapter - if (m_WaitForVblankEventParams.hAdapter != 0) { - D3DKMT_CLOSEADAPTER closeAdapterParams = {}; - closeAdapterParams.hAdapter = m_WaitForVblankEventParams.hAdapter; - m_D3DKMTCloseAdapter(&closeAdapterParams); + return false; } D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {}; @@ -115,7 +119,7 @@ void DxVsyncSource::waitForVsync() SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "CreateDC() failed: %d", GetLastError()); - return; + return false; } // Open the new adapter @@ -126,21 +130,38 @@ void DxVsyncSource::waitForVsync() SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "D3DKMTOpenAdapterFromHdc() failed: %x", status); - return; + return false; + } + + // VidPnSourceId is scoped to the adapter, so we must also check the adapter LUID + if (m_AdapterLuid.LowPart != openAdapterParams.AdapterLuid.LowPart || + m_AdapterLuid.HighPart != openAdapterParams.AdapterLuid.HighPart || + m_WaitForVblankEventParams.VidPnSourceId != openAdapterParams.VidPnSourceId) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "Monitor changed: %s %d Hz", + monitorInfo.szDevice, + monitorMode.dmDisplayFrequency); } m_WaitForVblankEventParams.hAdapter = openAdapterParams.hAdapter; m_WaitForVblankEventParams.hDevice = 0; m_WaitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId; + m_AdapterLuid = openAdapterParams.AdapterLuid; m_LastMonitor = currentMonitor; } status = m_D3DKMTWaitForVerticalBlankEvent(&m_WaitForVblankEventParams); if (status != STATUS_SUCCESS) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "D3DKMTWaitForVerticalBlankEvent() failed: %x", - status); - return; + if (status != STATUS_GRAPHICS_PRESENT_OCCLUDED) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "D3DKMTWaitForVerticalBlankEvent() failed: %x", + status); + } + + m_LastMonitor = nullptr; + return false; } + + return true; } diff --git a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h index f126df152f..836b8da9bb 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h +++ b/app/streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h @@ -10,6 +10,7 @@ typedef UINT D3DKMT_HANDLE; typedef UINT D3DDDI_VIDEO_PRESENT_SOURCE_ID; #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) #define STATUS_GRAPHICS_PRESENT_OCCLUDED ((NTSTATUS)0xC01E0006L) +#define D3DDDI_ID_UNINITIALIZED (UINT)(~0) typedef struct _D3DKMT_OPENADAPTERFROMHDC { HDC hDc; D3DKMT_HANDLE hAdapter; @@ -39,13 +40,14 @@ class DxVsyncSource : public IVsyncSource virtual bool isAsync() override; - virtual void waitForVsync() override; + virtual bool waitForVsync() override; private: Pacer* m_Pacer; HMODULE m_Gdi32Handle; HWND m_Window; HMONITOR m_LastMonitor; + LUID m_AdapterLuid; D3DKMT_WAITFORVERTICALBLANKEVENT m_WaitForVblankEventParams; PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc; diff --git a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp index 4ad1477829..2985df82fb 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp @@ -115,13 +115,20 @@ int Pacer::vsyncThread(void *context) while (!me->m_Stopping) { if (async) { // Wait for the VSync source to invoke signalVsync() or 100ms to elapse - me->m_FrameQueueLock.lock(); + QMutexLocker locker(&me->m_FrameQueueLock); me->m_VsyncSignalled.wait(&me->m_FrameQueueLock, 100); - me->m_FrameQueueLock.unlock(); } else { // Let the VSync source wait in the context of our thread - me->m_VsyncSource->waitForVsync(); + if (!me->m_VsyncSource->waitForVsync()) { + // Fall back to a fixed delay if the VSync source fails + // + // NB: We don't actually expect signalVsync() to be called, since + // this is a synchronous VSync source, but we wait on m_VsyncSignalled + // anyway so that the destructor can immediately wake us on shutdown. + QMutexLocker locker(&me->m_FrameQueueLock); + me->m_VsyncSignalled.wait(&me->m_FrameQueueLock, 1000 / me->m_DisplayFps); + } } if (me->m_Stopping) { @@ -148,23 +155,24 @@ int Pacer::renderThread(void* context) // Wait for the renderer to be ready for the next frame me->m_VsyncRenderer->waitToRender(); - // Acquire the frame queue lock to protect the queue and - // the not empty condition - me->m_FrameQueueLock.lock(); + AVFrame* frame; + { + // Acquire the frame queue lock to protect the queue and + // the not empty condition + QMutexLocker locker(&me->m_FrameQueueLock); - // Wait for a frame to be ready to render - while (!me->m_Stopping && me->m_RenderQueue.isEmpty()) { - me->m_RenderQueueNotEmpty.wait(&me->m_FrameQueueLock); - } + // Wait for a frame to be ready to render + while (!me->m_Stopping && me->m_RenderQueue.isEmpty()) { + me->m_RenderQueueNotEmpty.wait(&me->m_FrameQueueLock); + } - if (me->m_Stopping) { - // Exit this thread - me->m_FrameQueueLock.unlock(); - break; - } + if (me->m_Stopping) { + // Exit this thread + break; + } - AVFrame* frame = me->m_RenderQueue.dequeue(); - me->m_FrameQueueLock.unlock(); + frame = me->m_RenderQueue.dequeue(); + } me->renderFrame(frame); } @@ -349,7 +357,7 @@ void Pacer::renderFrame(AVFrame* frame) av_frame_free(&frame); // Drop frames if we have too many queued up for a while - m_FrameQueueLock.lock(); + QMutexLocker frameQueueLocker(&m_FrameQueueLock); int frameDropTarget; @@ -382,13 +390,11 @@ void Pacer::renderFrame(AVFrame* frame) AVFrame* frame = m_RenderQueue.dequeue(); // Drop the lock while we call av_frame_free() - m_FrameQueueLock.unlock(); + frameQueueLocker.unlock(); m_VideoStats->pacerDroppedFrames++; av_frame_free(&frame); - m_FrameQueueLock.lock(); + frameQueueLocker.relock(); } - - m_FrameQueueLock.unlock(); } void Pacer::dropFrameForEnqueue(QQueue& queue) diff --git a/app/streaming/video/ffmpeg-renderers/pacer/pacer.h b/app/streaming/video/ffmpeg-renderers/pacer/pacer.h index a1bb9eabb6..e55fe4bba7 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/pacer.h +++ b/app/streaming/video/ffmpeg-renderers/pacer/pacer.h @@ -22,9 +22,10 @@ class IVsyncSource { // sources require calls to waitForVsync(). virtual bool isAsync() = 0; - virtual void waitForVsync() { + virtual bool waitForVsync() { // Synchronous sources must implement waitForVsync()! SDL_assert(false); + return false; } }; From dc92b6d2f361064d3e9e53ce5de92f611db77220 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 20 Oct 2026 00:35:04 -0500 Subject: [PATCH 5/5] Update moonlight-common-c --- moonlight-common-c/moonlight-common-c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moonlight-common-c/moonlight-common-c b/moonlight-common-c/moonlight-common-c index f900dd4767..04053843c9 160000 --- a/moonlight-common-c/moonlight-common-c +++ b/moonlight-common-c/moonlight-common-c @@ -1 +1 @@ -Subproject commit f900dd4767759c7b9d0e93bcea666b55c69ea62f +Subproject commit 04053843c90c4268583f2496b997d54d59f03957