From c8474c515cacb2ba7d8372f5a7c72dee77a4d0b9 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 24 Oct 2026 18:17:00 -0500 Subject: [PATCH 01/18] Avoid signed overflow in bs_read_se() (#2071) bs_read_ue() can return values above INT32_MAX. bs_read_se() stored the result in an int32_t and computed (r+1)/2, which is undefined behavior when the code is 0x7FFFFFFF and gives wrong values for codes above INT32_MAX. The SPS being parsed comes from the host during the H.264 SPS fixup. Do the conversion in unsigned arithmetic. bs_read_ue() returns at most 0xFFFFFFFE, so both branches produce in-range int32_t values, and every code that was previously well-defined decodes to the same value. --- h264bitstream/bs.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/h264bitstream/bs.h b/h264bitstream/bs.h index fad69b0860..6929575614 100644 --- a/h264bitstream/bs.h +++ b/h264bitstream/bs.h @@ -178,16 +178,18 @@ static inline uint32_t bs_read_ue(bs_t* b) static inline int32_t bs_read_se(bs_t* b) { - int32_t r = bs_read_ue(b); + // Do the math in unsigned space. bs_read_ue() can return values above + // INT32_MAX, and (r+1)/2 on a signed INT32_MAX is undefined behavior. + // bs_read_ue() returns at most 0xFFFFFFFE, so both results fit in int32_t. + uint32_t r = bs_read_ue(b); if (r & 0x01) { - r = (r+1)/2; + return (int32_t)((r >> 1) + 1); } else { - r = -(r/2); + return -(int32_t)(r >> 1); } - return r; } static inline void bs_write_u1(bs_t* b, uint32_t v) From 8f2e52e54046fc942e2b80ffb88a71ff0f2300bd Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 24 Oct 2026 18:19:58 -0500 Subject: [PATCH 02/18] Refuse to open a pre-existing log file (#2068) On macOS, release builds log to /tmp/Moonlight-.log. /tmp is shared by all users and the file name is predictable, so another local user could plant symlinks at upcoming log names and have Moonlight truncate and overwrite any file the victim can write to. The log was also created world-readable. Open the log with QIODevice::NewOnly (O_CREAT|O_EXCL) so an existing file or symlink is never opened, and create it readable only by the owner on Qt 6.3+. If the open fails we keep logging to stderr, as we already did for any other open failure. --- app/main.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/main.cpp b/app/main.cpp index 7ec0cca944..6629a75282 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -465,7 +465,16 @@ int main(int argc, char *argv[]) #endif { s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch()))); - if (s_LoggerFile->open(QIODevice::WriteOnly | QIODevice::Text)) { + + // The log directory may be shared with other users (/tmp on macOS), so + // we must never open a file or symlink that someone else planted there. + // NewOnly fails if anything already exists at this path. + QIODevice::OpenMode logOpenMode = QIODevice::WriteOnly | QIODevice::Text | QIODevice::NewOnly; +#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) + if (s_LoggerFile->open(logOpenMode, QFileDevice::ReadOwner | QFileDevice::WriteOwner)) { +#else + if (s_LoggerFile->open(logOpenMode)) { +#endif QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl; s_LoggerStream.setDevice(s_LoggerFile); } From 40f83bd294af4a32c66a9b1038d4be15b640c7a5 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 16:57:55 -0500 Subject: [PATCH 03/18] Avoid wedging VDPAU renderer if mixer creation fails --- app/streaming/video/ffmpeg-renderers/vdpau.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/vdpau.cpp b/app/streaming/video/ffmpeg-renderers/vdpau.cpp index dc0522e866..f1423a8ea3 100644 --- a/app/streaming/video/ffmpeg-renderers/vdpau.cpp +++ b/app/streaming/video/ffmpeg-renderers/vdpau.cpp @@ -541,11 +541,12 @@ void VDPAURenderer::renderFrame(AVFrame* frame) // We need to create the mixer on the fly, because we don't know the dimensions // of our video surfaces in advance of decoding. We also need to recreate it when // the frame format or size changes. - if (hasFrameFormatChanged(frame)) { - if (m_VideoMixer != 0) { - m_VdpVideoMixerDestroy(m_VideoMixer); - } + if (hasFrameFormatChanged(frame) && m_VideoMixer != 0) { + m_VdpVideoMixerDestroy(m_VideoMixer); + m_VideoMixer = 0; + } + if (m_VideoMixer == 0) { VdpChromaType videoSurfaceChroma; uint32_t videoSurfaceWidth, videoSurfaceHeight; status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma, @@ -580,6 +581,7 @@ void VDPAURenderer::renderFrame(AVFrame* frame) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "VdpVideoMixerCreate() failed: %s", m_VdpGetErrorString(status)); + m_VideoMixer = 0; return; } } From 1826d4c23299bfaa1b0ee9e57fdee50986e89175 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 17:00:11 -0500 Subject: [PATCH 04/18] Fix incorrect mmap() failure check --- app/streaming/video/ffmpeg-renderers/drm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/streaming/video/ffmpeg-renderers/drm.cpp b/app/streaming/video/ffmpeg-renderers/drm.cpp index 50d7a3950b..3ac5c5b55b 100644 --- a/app/streaming/video/ffmpeg-renderers/drm.cpp +++ b/app/streaming/video/ffmpeg-renderers/drm.cpp @@ -1307,7 +1307,7 @@ bool DrmRenderer::mapDumbBuffer(uint32_t handle, size_t size, void** mapping) #else *mapping = mmap(nullptr, size, PROT_WRITE, MAP_SHARED, m_DrmFd, mapBuf.offset); #endif - if (mapping == MAP_FAILED) { + if (*mapping == MAP_FAILED) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "mmap() failed for dumb buffer: %d", errno); From 0c4f7aefb6b0b72a025f57fc4e03a50dc496c907 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:37:28 -0500 Subject: [PATCH 05/18] Handle unexpected appVersion/gfeVersion quads --- app/backend/nvpairingmanager.cpp | 2 +- app/streaming/session.cpp | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/backend/nvpairingmanager.cpp b/app/backend/nvpairingmanager.cpp index 665e7e863a..59be94ea43 100644 --- a/app/backend/nvpairingmanager.cpp +++ b/app/backend/nvpairingmanager.cpp @@ -215,7 +215,7 @@ NvPairingManager::saltPin(const QByteArray& salt, QString pin) NvPairingManager::PairState NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert) { - int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0); + int serverMajorVersion = NvHTTP::parseQuad(appVersion).value(0); qInfo() << "Pairing with server generation:" << serverMajorVersion; QCryptographicHash::Algorithm hashAlgo; diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index c6e3470d14..3898c00f92 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -840,9 +840,8 @@ bool Session::initialize(QQuickWindow* qtWindow) // to allow decoding via VideoToolbox on macOS. Since we don't // have fixup code for HEVC, just avoid it if GFE is too old. QVector gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion); - if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all - gfeVersion[0] < 3 || - (gfeVersion[0] == 3 && gfeVersion[1] < 11)) { + if (gfeVersion.value(0) < 3 || + (gfeVersion.value(0) == 3 && gfeVersion.value(1) < 11)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Disabling HEVC on macOS due to old GFE version"); m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_H265); From 372887b8a29c5abf69aaebe8d0001f4edb9d40a1 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:39:23 -0500 Subject: [PATCH 06/18] Don't copy unpopulated EGLAttribs --- app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp b/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp index 2faf4b0aee..dcb21591ca 100644 --- a/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp @@ -240,7 +240,7 @@ ssize_t EglImageFactory::exportDRMImages(AVFrame* frame, EGLDisplay dpy, EGLImag else { // Cast the EGLAttrib array elements to EGLint for the KHR extension EGLint intAttribs[MAX_ATTRIB_COUNT]; - for (int i = 0; i < MAX_ATTRIB_COUNT; i++) { + for (int i = 0; i < attribIndex; i++) { intAttribs[i] = (EGLint)attribs[i]; } From 4158fa782391d8b71e010c657dac53585a542560 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:40:58 -0500 Subject: [PATCH 07/18] Don't add EGL modifiers for VA images if they aren't set --- app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp b/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp index dcb21591ca..91ddcf43fd 100644 --- a/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglimagefactory.cpp @@ -328,7 +328,7 @@ ssize_t EglImageFactory::exportVAImages(AVFrame *frame, uint32_t exportFlags, EG attribs[attribIndex++] = layer.offset[0]; attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; attribs[attribIndex++] = layer.pitch[0]; - if (m_EGLExtDmaBuf) { + if (m_EGLExtDmaBuf && object.drm_format_modifier != DRM_FORMAT_MOD_INVALID) { attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; attribs[attribIndex++] = (EGLint)(object.drm_format_modifier & 0xFFFFFFFF); attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; @@ -343,7 +343,7 @@ ssize_t EglImageFactory::exportVAImages(AVFrame *frame, uint32_t exportFlags, EG attribs[attribIndex++] = layer.offset[1]; attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_PITCH_EXT; attribs[attribIndex++] = layer.pitch[1]; - if (m_EGLExtDmaBuf) { + if (m_EGLExtDmaBuf && object.drm_format_modifier != DRM_FORMAT_MOD_INVALID) { attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT; attribs[attribIndex++] = (EGLint)(object.drm_format_modifier & 0xFFFFFFFF); attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT; @@ -358,7 +358,7 @@ ssize_t EglImageFactory::exportVAImages(AVFrame *frame, uint32_t exportFlags, EG attribs[attribIndex++] = layer.offset[2]; attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_PITCH_EXT; attribs[attribIndex++] = layer.pitch[2]; - if (m_EGLExtDmaBuf) { + if (m_EGLExtDmaBuf && object.drm_format_modifier != DRM_FORMAT_MOD_INVALID) { attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT; attribs[attribIndex++] = (EGLint)(object.drm_format_modifier & 0xFFFFFFFF); attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT; @@ -373,7 +373,7 @@ ssize_t EglImageFactory::exportVAImages(AVFrame *frame, uint32_t exportFlags, EG attribs[attribIndex++] = layer.offset[3]; attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_PITCH_EXT; attribs[attribIndex++] = layer.pitch[3]; - if (m_EGLExtDmaBuf) { + if (m_EGLExtDmaBuf && object.drm_format_modifier != DRM_FORMAT_MOD_INVALID) { attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT; attribs[attribIndex++] = (EGLint)(object.drm_format_modifier & 0xFFFFFFFF); attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT; From 2b690d14ad8789eebfa8a5a178716c49324a2998 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:44:17 -0500 Subject: [PATCH 08/18] Initialize VAAPIRenderer class members --- app/streaming/video/ffmpeg-renderers/vaapi.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.cpp b/app/streaming/video/ffmpeg-renderers/vaapi.cpp index 90e042eb92..9981c6ed0e 100644 --- a/app/streaming/video/ffmpeg-renderers/vaapi.cpp +++ b/app/streaming/video/ffmpeg-renderers/vaapi.cpp @@ -21,8 +21,10 @@ VAAPIRenderer::VAAPIRenderer(int decoderSelectionPass) : IFFmpegRenderer(RendererType::VAAPI), m_DecoderSelectionPass(decoderSelectionPass), + m_WindowSystem(SDL_SYSWM_UNKNOWN), m_HwContext(nullptr), m_BlacklistedForDirectRendering(false), + m_HasRfiLatencyBug(false), m_RequiresExplicitPixelFormat(false), m_OverlayMutex(nullptr) #ifdef HAVE_EGL From bfc6751f0ff730ccd5f50ae97ec1ed6cfc013144 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:46:33 -0500 Subject: [PATCH 09/18] Fix fd leak in VAAPI KMSDRM codepath --- app/streaming/video/ffmpeg-renderers/vaapi.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/streaming/video/ffmpeg-renderers/vaapi.cpp b/app/streaming/video/ffmpeg-renderers/vaapi.cpp index 9981c6ed0e..05bc709ed3 100644 --- a/app/streaming/video/ffmpeg-renderers/vaapi.cpp +++ b/app/streaming/video/ffmpeg-renderers/vaapi.cpp @@ -161,6 +161,9 @@ VAAPIRenderer::openDisplay(SDL_Window* window) errno); return nullptr; } + // This FD is owned by us (not shared with SDL), so it must be + // closed when we're done with it or adopted into m_DrmFd. + mustCloseFd = true; } // If the KMSDRM FD is not a render node FD, open the render node for libva to use. From df7db4018e304d9ff531bfa0fc7f5857570a8ebe Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:47:56 -0500 Subject: [PATCH 10/18] Fix some EGL error handling paths --- app/streaming/video/ffmpeg-renderers/eglvid.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.cpp b/app/streaming/video/ffmpeg-renderers/eglvid.cpp index e5090b6eb8..bb9e890448 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp @@ -269,7 +269,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type, int viewportWidth, in int EGLRenderer::loadAndBuildShader(int shaderType, const char *file) { GLuint shader = glCreateShader(shaderType); - if (!shader || shader == GL_INVALID_ENUM) { + if (!shader) { EGL_LOG(Error, "Can't create shader: %d", glGetError()); return 0; } @@ -286,6 +286,7 @@ int EGLRenderer::loadAndBuildShader(int shaderType, char shaderLog[512]; glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog); EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog); + glDeleteShader(shader); return 0; } @@ -297,7 +298,7 @@ unsigned EGLRenderer::compileShader(const char* vertexShaderSrc, const char* fra GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, vertexShaderSrc); if (!vertexShader) - return false; + return 0; GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, fragmentShaderSrc); if (!fragmentShader) @@ -787,7 +788,7 @@ void EGLRenderer::renderFrame(AVFrame* frame) } ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs); - if (plane_count < 0) + if (plane_count <= 0) return; for (ssize_t i = 0; i < plane_count; ++i) { glActiveTexture(GL_TEXTURE0 + i); From fb9daf85ab4dfbf3deb45c9361855e0cd8da6be7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 18:49:28 -0500 Subject: [PATCH 11/18] Keep the VBO alive while the VAO references it --- app/streaming/video/ffmpeg-renderers/eglvid.cpp | 10 ++++++---- app/streaming/video/ffmpeg-renderers/eglvid.h | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.cpp b/app/streaming/video/ffmpeg-renderers/eglvid.cpp index bb9e890448..bb2f2036b8 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp @@ -63,6 +63,7 @@ EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer) m_Window(nullptr), m_Backend(backendRenderer), m_VideoVAO(0), + m_VideoVBO(0), m_BlockingSwapBuffers(false), m_LastRenderSync(EGL_NO_SYNC), m_glEGLImageTargetTexture2DOES(nullptr), @@ -100,6 +101,7 @@ EGLRenderer::~EGLRenderer() SDL_assert(m_glDeleteVertexArraysOES != nullptr); m_glDeleteVertexArraysOES(1, &m_VideoVAO); } + glDeleteBuffers(1, &m_VideoVBO); glDeleteTextures(EGL_MAX_PLANES, m_Textures); glDeleteTextures(Overlay::OverlayMax, m_OverlayTextures); @@ -646,13 +648,12 @@ bool EGLRenderer::setupVideoRenderingState() { }; // Setup the VAO and VBO - unsigned int VBO; m_glGenVertexArraysOES(1, &m_VideoVAO); - glGenBuffers(1, &VBO); + glGenBuffers(1, &m_VideoVBO); m_glBindVertexArrayOES(m_VideoVAO); - glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBindBuffer(GL_ARRAY_BUFFER, m_VideoVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // compileShader() ensures that aPosition and aTexCoord are indexes 0 and 1 respectively @@ -664,7 +665,8 @@ bool EGLRenderer::setupVideoRenderingState() { glBindBuffer(GL_ARRAY_BUFFER, 0); m_glBindVertexArrayOES(0); - glDeleteBuffers(1, &VBO); + // The VBO is kept alive as a member since the VAO still references it. + // It is deleted in ~EGLRenderer() alongside the other GL resources. GLenum err = glGetError(); if (err != GL_NO_ERROR) { diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.h b/app/streaming/video/ffmpeg-renderers/eglvid.h index 4528dc7bae..f3467d8299 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.h +++ b/app/streaming/video/ffmpeg-renderers/eglvid.h @@ -44,6 +44,7 @@ class EGLRenderer : public IFFmpegRenderer { SDL_Window *m_Window; IFFmpegRenderer *m_Backend; unsigned int m_VideoVAO; + unsigned int m_VideoVBO; bool m_BlockingSwapBuffers; EGLSync m_LastRenderSync; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES; From 29a280eb388ff0adefcb63d2df347e0b4a6bebff Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 19:46:14 -0500 Subject: [PATCH 12/18] Fix return type of EGLRenderer::loadAndBuildShader() --- app/streaming/video/ffmpeg-renderers/eglvid.cpp | 3 +-- app/streaming/video/ffmpeg-renderers/eglvid.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.cpp b/app/streaming/video/ffmpeg-renderers/eglvid.cpp index bb2f2036b8..0f4f37908f 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.cpp +++ b/app/streaming/video/ffmpeg-renderers/eglvid.cpp @@ -268,8 +268,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type, int viewportWidth, in glDisable(GL_BLEND); } -int EGLRenderer::loadAndBuildShader(int shaderType, - const char *file) { +GLuint EGLRenderer::loadAndBuildShader(int shaderType, const char *file) { GLuint shader = glCreateShader(shaderType); if (!shader) { EGL_LOG(Error, "Can't create shader: %d", glGetError()); diff --git a/app/streaming/video/ffmpeg-renderers/eglvid.h b/app/streaming/video/ffmpeg-renderers/eglvid.h index f3467d8299..89793c532f 100644 --- a/app/streaming/video/ffmpeg-renderers/eglvid.h +++ b/app/streaming/video/ffmpeg-renderers/eglvid.h @@ -29,7 +29,7 @@ class EGLRenderer : public IFFmpegRenderer { bool compileShaders(); bool setupVideoRenderingState(); bool setupOverlayRenderingState(); - static int loadAndBuildShader(int shaderType, const char *filename); + static GLuint loadAndBuildShader(int shaderType, const char *filename); AVPixelFormat m_EGLImagePixelFormat; void *m_EGLDisplay; From 465c41b5c028fce54d2d7c91299feab0c2ec396f Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 13:52:34 -0500 Subject: [PATCH 13/18] Fix uninitialized InitFailureReason field --- app/streaming/video/ffmpeg-renderers/renderer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/streaming/video/ffmpeg-renderers/renderer.h b/app/streaming/video/ffmpeg-renderers/renderer.h index ba125842e7..37754c7509 100644 --- a/app/streaming/video/ffmpeg-renderers/renderer.h +++ b/app/streaming/video/ffmpeg-renderers/renderer.h @@ -533,7 +533,7 @@ class IFFmpegRenderer : public Overlay::IOverlayRenderer { #endif protected: - InitFailureReason m_InitFailureReason; + InitFailureReason m_InitFailureReason = InitFailureReason::Unknown; private: RendererType m_Type; From ba700d67ececc1126887d7067b092d5831c68128 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 14:16:54 -0500 Subject: [PATCH 14/18] Fix shadowed member variable with base class --- app/streaming/video/ffmpeg-renderers/d3d11va.cpp | 6 +++--- app/streaming/video/ffmpeg-renderers/d3d11va.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/streaming/video/ffmpeg-renderers/d3d11va.cpp b/app/streaming/video/ffmpeg-renderers/d3d11va.cpp index fed027f1a0..058a9f1922 100644 --- a/app/streaming/video/ffmpeg-renderers/d3d11va.cpp +++ b/app/streaming/video/ffmpeg-renderers/d3d11va.cpp @@ -60,7 +60,7 @@ D3D11VARenderer::D3D11VARenderer(int decoderSelectionPass) m_DecoderSelectionPass(decoderSelectionPass), m_DevicesWithFL11Support(0), m_DevicesWithCodecSupport(0), - m_LastColorTrc(AVCOL_TRC_UNSPECIFIED), + m_SwapChainColorTrc(AVCOL_TRC_UNSPECIFIED), m_AllowTearing(false), m_OverlayLock(0), m_HwDeviceContext(nullptr) @@ -800,7 +800,7 @@ void D3D11VARenderer::renderFrame(AVFrame* frame) HRESULT hr; - if (frame->color_trc != m_LastColorTrc) { + if (frame->color_trc != m_SwapChainColorTrc) { if (frame->color_trc == AVCOL_TRC_SMPTE2084) { // Switch to Rec 2020 PQ (SMPTE ST 2084) colorspace for HDR10 rendering hr = m_SwapChain->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); @@ -820,7 +820,7 @@ void D3D11VARenderer::renderFrame(AVFrame* frame) } } - m_LastColorTrc = frame->color_trc; + m_SwapChainColorTrc = frame->color_trc; } // Present according to the decoder parameters diff --git a/app/streaming/video/ffmpeg-renderers/d3d11va.h b/app/streaming/video/ffmpeg-renderers/d3d11va.h index 864ad59d5c..aeab680e55 100644 --- a/app/streaming/video/ffmpeg-renderers/d3d11va.h +++ b/app/streaming/video/ffmpeg-renderers/d3d11va.h @@ -90,7 +90,7 @@ class D3D11VARenderer : public IFFmpegRenderer DXGI_FORMAT m_TextureFormat; int m_DisplayWidth; int m_DisplayHeight; - AVColorTransferCharacteristic m_LastColorTrc; + AVColorTransferCharacteristic m_SwapChainColorTrc; bool m_AllowTearing; From d606709d83f4782d6540c3fdeb4958eb4ae4b2d2 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 14:17:44 -0500 Subject: [PATCH 15/18] Add some missing error handling --- app/streaming/video/ffmpeg-renderers/dxva2.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/streaming/video/ffmpeg-renderers/dxva2.cpp b/app/streaming/video/ffmpeg-renderers/dxva2.cpp index e43861576c..064e5af288 100644 --- a/app/streaming/video/ffmpeg-renderers/dxva2.cpp +++ b/app/streaming/video/ffmpeg-renderers/dxva2.cpp @@ -426,7 +426,13 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync) D3DDISPLAYMODEEX currentMode; currentMode.Size = sizeof(currentMode); - d3d9ex->GetAdapterDisplayModeEx(adapterIndex, ¤tMode, nullptr); + hr = d3d9ex->GetAdapterDisplayModeEx(adapterIndex, ¤tMode, nullptr); + if (FAILED(hr)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "GetAdapterDisplayModeEx() failed: %x", + hr); + return false; + } D3DPRESENT_PARAMETERS d3dpp = {}; d3dpp.hDeviceWindow = info.info.win.window; @@ -987,6 +993,7 @@ void DXVA2Renderer::renderFrame(AVFrame *frame) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "StretchRect() failed: %x", hr); + m_Device->EndScene(); SDL_Event event; event.type = SDL_RENDER_DEVICE_RESET; SDL_PushEvent(&event); From 9fee675c91ba6e37dfeb343c7811af0812e90524 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 20:26:29 -0500 Subject: [PATCH 16/18] Fix controller number check for adaptive triggers --- app/streaming/input/gamepad.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/streaming/input/gamepad.cpp b/app/streaming/input/gamepad.cpp index b649971b69..d4e4c03f22 100644 --- a/app/streaming/input/gamepad.cpp +++ b/app/streaming/input/gamepad.cpp @@ -965,7 +965,7 @@ void SdlInputHandler::setAdaptiveTriggers(uint16_t controllerNumber, DualSenseOu #if SDL_VERSION_ATLEAST(2, 0, 16) // Make sure the controller number is within our supported count - if (controllerNumber <= MAX_GAMEPADS && + if (controllerNumber < MAX_GAMEPADS && // and we have a valid controller m_GamepadState[controllerNumber].controller != nullptr && // and it's a PS5 controller From 6fc7e816830bd06cacce78e4b310e55638c9428d Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 21:26:32 -0500 Subject: [PATCH 17/18] Handle H.264 SPS read/write failure in fixup path --- app/streaming/video/ffmpeg.cpp | 35 +++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/app/streaming/video/ffmpeg.cpp b/app/streaming/video/ffmpeg.cpp index c20d312667..fab14b9788 100644 --- a/app/streaming/video/ffmpeg.cpp +++ b/app/streaming/video/ffmpeg.cpp @@ -1765,11 +1765,21 @@ void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset) int nalStart, nalEnd; bool needsFixup; - // Read the old NALU - find_nal_unit((uint8_t*)entry->data, entry->length, &nalStart, &nalEnd); - read_nal_unit(stream, - (unsigned char *)&entry->data[nalStart], - nalEnd - nalStart); + // Find the old SPS NALU + if (find_nal_unit((uint8_t*)entry->data, entry->length, &nalStart, &nalEnd) <= 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to find SPS"); + h264_free(stream); + goto WriteUnmodifiedBuffer; + } + + // Read the SPS. Pass it through unmodified if we fail to parse it. + if (read_nal_unit(stream, (uint8_t*)&entry->data[nalStart], nalEnd - nalStart) < 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to read SPS"); + h264_free(stream); + goto WriteUnmodifiedBuffer; + } SDL_assert(nalStart == 3 || nalStart == 4); // 3 or 4 byte Annex B start sequence SDL_assert(nalEnd == entry->length); @@ -1802,8 +1812,18 @@ void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset) // Copy the modified NALU data. This clobbers byte 0 and starts NALU data at byte 1. // Since it prepended one extra byte, subtract one from the returned length. - offset += write_nal_unit(stream, (uint8_t*)&m_DecodeBuffer.data()[initialOffset + nalStart - 1], - MAX_SPS_EXTRA_SIZE + entry->length - nalStart) - 1; + // + // Pass the SPS through unmodified if the write fails. + int writeLength = write_nal_unit(stream, (uint8_t*)&m_DecodeBuffer.data()[initialOffset + nalStart - 1], + MAX_SPS_EXTRA_SIZE + entry->length - nalStart); + if (writeLength < 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "Failed to write modified SPS"); + h264_free(stream); + goto WriteUnmodifiedBuffer; + } + + offset += writeLength - 1; // Copy the NALU prefix over from the original SPS memcpy(&m_DecodeBuffer.data()[initialOffset], entry->data, nalStart); @@ -1834,6 +1854,7 @@ void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset) h264_free(stream); } else { +WriteUnmodifiedBuffer: // Write the buffer as-is memcpy(&m_DecodeBuffer.data()[offset], entry->data, From ac296864ace96f51ad75f78b90644bdbca76c332 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 24 Oct 2026 22:41:06 -0500 Subject: [PATCH 18/18] Fix some error handling in the libplacebo renderer --- app/streaming/video/ffmpeg-renderers/plvk.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/streaming/video/ffmpeg-renderers/plvk.cpp b/app/streaming/video/ffmpeg-renderers/plvk.cpp index 3a7b2a105d..07ccce8fff 100644 --- a/app/streaming/video/ffmpeg-renderers/plvk.cpp +++ b/app/streaming/video/ffmpeg-renderers/plvk.cpp @@ -769,6 +769,7 @@ bool PlVkRenderer::populateQueues(int videoFormat) vkDeviceContext->nb_comp_queues = m_Vulkan->queue_compute.count; // Select a video decode queue that is capable of decoding our chosen format + vkDeviceContext->queue_family_decode_index = -1; for (uint32_t i = 0; i < queueFamilyCount; i++) { if (queueFamilies[i].queueFamilyProperties.queueFlags & VK_QUEUE_VIDEO_DECODE_BIT_KHR) { if (videoFormat & VIDEO_FORMAT_MASK_H264) { @@ -897,6 +898,16 @@ void PlVkRenderer::waitToRender() return; } + // If a swapchain (re)creation failed, m_Swapchain is NULL. We can't render + // or resize it, and a device reset has already been requested, so skip this + // frame and let the main thread recreate the renderer. + if (m_Swapchain == nullptr) { + SDL_Event event; + event.type = SDL_RENDER_DEVICE_RESET; + SDL_PushEvent(&event); + return; + } + #ifndef Q_OS_WIN32 // With libplacebo's Vulkan backend, all swap_buffers does is wait for queued // presents to finish. This happens to be exactly what we want to do here, since