From 178de1ed6408d2ff6204077eb3ffc7b1b020b849 Mon Sep 17 00:00:00 2001 From: Sjoerd Siebinga Date: Tue, 14 Apr 2026 15:03:08 +0200 Subject: [PATCH 1/3] core/Compositor: null-guard CWLSurfaceResource against expired m_resource CWLSurfaceResource::good() returns whether the underlying wayland resource is alive, but it dereferences m_resource without checking that m_resource itself is non-null. If the surface's CWpSurfaceResource has been destroyed while callers still hold SP references, good() segfaults instead of returning false as expected. Fix good() to check m_resource first, and have the methods that forward to m_resource (enter, leave, sendPreferredTransform, sendPreferredScale, id, error) bail early when !good() so the contract is actually honoured. Also null-check the pSurface argument in CCompositor::setPreferredScaleForSurface and setPreferredTransformForSurface, both of which otherwise deref a possibly-null surface pointer on their first PROTO/sendPreferred call. Observed as a SIGSEGV in surface preferred-scale/transform notification paths when a monitor re-emits its preferred state after a protocol resource had already been torn down. --- src/Compositor.cpp | 6 ++++++ src/protocols/core/Compositor.cpp | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Compositor.cpp b/src/Compositor.cpp index d40f3ce86..f6e23c950 100644 --- a/src/Compositor.cpp +++ b/src/Compositor.cpp @@ -2904,6 +2904,9 @@ void CCompositor::leaveUnsafeState() { } void CCompositor::setPreferredScaleForSurface(SP pSurface, double scale) { + if (!pSurface) + return; + PROTO::fractional->sendScale(pSurface, scale); pSurface->sendPreferredScale(std::ceil(scale)); @@ -2918,6 +2921,9 @@ void CCompositor::setPreferredScaleForSurface(SP pSurface, d } void CCompositor::setPreferredTransformForSurface(SP pSurface, wl_output_transform transform) { + if (!pSurface) + return; + pSurface->sendPreferredTransform(transform); const auto PSURFACE = Desktop::View::CWLSurface::fromResource(pSurface); diff --git a/src/protocols/core/Compositor.cpp b/src/protocols/core/Compositor.cpp index a51c60a2f..e7d21867c 100644 --- a/src/protocols/core/Compositor.cpp +++ b/src/protocols/core/Compositor.cpp @@ -264,7 +264,7 @@ SP CWLSurfaceResource::fromResource(wl_resource* res) { } bool CWLSurfaceResource::good() { - return m_resource->resource(); + return m_resource && m_resource->resource(); } wl_client* CWLSurfaceResource::client() { @@ -272,6 +272,9 @@ wl_client* CWLSurfaceResource::client() { } void CWLSurfaceResource::enter(PHLMONITOR monitor) { + if (!good()) + return; + if (std::ranges::find(m_enteredOutputs, monitor) != m_enteredOutputs.end()) return; @@ -304,6 +307,9 @@ void CWLSurfaceResource::enter(PHLMONITOR monitor) { } void CWLSurfaceResource::leave(PHLMONITOR monitor) { + if (!good()) + return; + if UNLIKELY (std::ranges::find(m_enteredOutputs, monitor) == m_enteredOutputs.end()) return; @@ -325,12 +331,16 @@ void CWLSurfaceResource::leave(PHLMONITOR monitor) { } void CWLSurfaceResource::sendPreferredTransform(wl_output_transform t) { + if (!good()) + return; if (m_resource->version() < 6) return; m_resource->sendPreferredBufferTransform(t); } void CWLSurfaceResource::sendPreferredScale(int32_t scale) { + if (!good()) + return; if (m_resource->version() < 6) return; m_resource->sendPreferredBufferScale(scale); @@ -471,6 +481,8 @@ std::pair, Vector2D> CWLSurfaceResource::at(const Vector2 } uint32_t CWLSurfaceResource::id() { + if (!good()) + return 0; return wl_resource_get_id(m_resource->resource()); } @@ -505,6 +517,8 @@ void CWLSurfaceResource::releaseBuffers(bool onlyCurrent) { } void CWLSurfaceResource::error(int code, const std::string& str) { + if (!good()) + return; m_resource->error(code, str); } From 15357c9685526698a4a9962e7460e4f1eab6d283 Mon Sep 17 00:00:00 2001 From: Sjoerd Siebinga Date: Tue, 14 Apr 2026 15:13:14 +0200 Subject: [PATCH 2/3] render: null-guard blur framebuffer accesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monitor's pre-blur framebuffer (m_blurFB) can be null when a blur is attempted — typically during monitor (re)initialization, after a GPU reset that invalidates framebuffer objects, or when introspection has been disabled. Several call sites dereferenced it unconditionally. Add null checks in: - IHyprRenderer::getBlurTexture — return nullptr if either the monitor or its blurFB is missing, rather than deref-crashing. - IHyprRenderer::blurMainFramebuffer — in the existing "BUG THIS" error path that tries to return *something* to sample from, guard the fallback so a null blurFB yields nullptr instead of a segfault. - IHyprRenderer::preBlurForCurrentMonitor — log and early-return if m_blurFB is null, rather than binding a null framebuffer. - CHyprOpenGLImpl::renderRectWithBlurInternal — if the resolved blur texture is null (either the xray fast path or the blur-main path failed), log and skip the blur pass instead of passing null down. Skipping blur is always preferable to crashing the compositor. Healthy frames are unaffected. --- src/render/OpenGL.cpp | 7 ++++++- src/render/Renderer.cpp | 11 +++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index bf12ac8de..87d742d6a 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -1046,7 +1046,12 @@ void CHyprOpenGLImpl::renderRectWithBlurInternal(const CBox& box, const CHyprCol CRegion damage{g_pHyprRenderer->m_renderData.damage}; damage.intersect(box); - auto blurredBG = data.xray ? g_pHyprRenderer->m_renderData.pMonitor->resources()->m_blurFB->getTexture() : g_pHyprRenderer->blurMainFramebuffer(data.blurA, &damage); + auto blurFB = g_pHyprRenderer->m_renderData.pMonitor->resources()->m_blurFB; + auto blurredBG = data.xray ? (blurFB ? blurFB->getTexture() : nullptr) : g_pHyprRenderer->blurMainFramebuffer(data.blurA, &damage); + if (!blurredBG) { + Log::logger->log(Log::ERR, "renderTextureWithBlur: blur texture unavailable (likely GPU reset). Skipping blur."); + return; + } CBox MONITORBOX = {0, 0, g_pHyprRenderer->m_renderData.pMonitor->m_transformedSize.x, g_pHyprRenderer->m_renderData.pMonitor->m_transformedSize.y}; g_pHyprRenderer->pushMonitorTransformEnabled(true); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index f5855c440..1e8f211f4 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -1395,6 +1395,8 @@ SP IHyprRenderer::loadAsset(const std::string& filename) { } SP IHyprRenderer::getBlurTexture(PHLMONITORREF pMonitor) { + if (!pMonitor || !pMonitor->resources()->m_blurFB) + return nullptr; return pMonitor->resources()->m_blurFB->getTexture(); } @@ -1721,9 +1723,10 @@ Mat3x3 IHyprRenderer::projectBoxToTarget(const CBox& box, std::optional IHyprRenderer::blurMainFramebuffer(float a, CRegion* originalDamage) { - if (!m_renderData.currentFB->getTexture()) { + if (!m_renderData.currentFB || !m_renderData.currentFB->getTexture()) { Log::logger->log(Log::ERR, "BUG THIS: null fb texture while attempting to blur main fb?! (introspection off?!)"); - return m_renderData.pMonitor->resources()->m_blurFB->getTexture(); // return something to sample from at least + auto blurFB = m_renderData.pMonitor->resources()->m_blurFB; + return blurFB ? blurFB->getTexture() : nullptr; } auto guard = bindTempFB(m_renderData.currentFB); // blurFramebuffer messes with FB bindings @@ -1735,6 +1738,10 @@ void IHyprRenderer::preBlurForCurrentMonitor(CRegion* fakeDamage) { const auto blurredTex = blurMainFramebuffer(1, fakeDamage); // render onto blurFB + if (!m_renderData.pMonitor->resources()->m_blurFB) { + Log::logger->log(Log::ERR, "preBlurForCurrentMonitor: blurFB is null, skipping blur."); + return; + } auto guard = bindTempFB(m_renderData.pMonitor->resources()->m_blurFB); const auto SAVE_TRANSFORM = blurredTex->m_transform; blurredTex->m_transform = Math::wlTransformToHyprutils(Math::invertTransform(m_renderData.pMonitor->m_transform)); From 0603a354f3cf8c1c42b6c40b6022dd27bddc2b74 Mon Sep 17 00:00:00 2001 From: Sjoerd Siebinga Date: Tue, 14 Apr 2026 15:14:31 +0200 Subject: [PATCH 3/3] render: replace fatal aborts on GPU reset with EGL context recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hyprland currently triggers RASSERT (SIGABRT) on three GPU-reset signals: glGetGraphicsResetStatus() reporting a reset in begin() / beginSimple(), glGetError() returning GL_CONTEXT_LOST at end(), and renderTextureInternal() receiving an invalid texture. The original abort message acknowledged this as a gap: "Cannot continue until proper GPU reset handling is implemented." On recent Intel (Xe driver on Meteor Lake/Arc graphics), TLB invalidation timeouts cause the kernel to issue recoverable GPU resets and re-arm the rendering engines without losing the EGL display. The compositor is expected to re-establish its context and carry on. The current RASSERT takes down the entire user session on what is, from the kernel's perspective, a handled fault — and these resets can cluster during suspend/resume or under memory pressure. This patch adds a minimal recovery mechanism: - attemptContextReset() unbinds and rebinds the EGL context via eglMakeCurrent(EGL_NO_CONTEXT) then eglMakeCurrent(m_eglContext), and marks shaders for reinitialization on the next frame. - begin() and beginSimple() check a per-monitor m_gpuResetCooldown counter first; if > 0, they skip the frame and decrement. Otherwise, if glGetGraphicsResetStatus reports a reset, they log the reason, call attemptContextReset(), and either skip one frame (on success) or set a 60-frame cooldown (on failure) to avoid tight-loop recovery attempts. pMonitor is reset so the skipped frame doesn't hold a dangling reference. - end() no longer aborts on GL_CONTEXT_LOST; it logs and sets the cooldown, letting the next begin() drive recovery. - renderTextureInternal() replaces the RASSERT-on-invalid-texture with a logged skip. Post-reset textures are often invalid until reuploaded; skipping the draw for one frame is preferable to SIGABRT. The existing RASSERT(pMonitor, "...without begin()!") is kept — that's a programmer error, not a recoverable state. Tested on a Framework 13 / Intel Xe setup that was previously crashing on GPU resets; with this patch the compositor logs the reset, skips a handful of frames, and resumes rendering normally. Combined with the prior two commits (surface and blur null-guards), this covers the follow-on nulls that surface during the recovery window. --- src/render/OpenGL.cpp | 59 ++++++++++++++++++++++++++++++++++++++----- src/render/OpenGL.hpp | 2 ++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/render/OpenGL.cpp b/src/render/OpenGL.cpp index 87d742d6a..2b57ba9f4 100644 --- a/src/render/OpenGL.cpp +++ b/src/render/OpenGL.cpp @@ -663,9 +663,28 @@ EGLImageKHR CHyprOpenGLImpl::createEGLImage(const Aquamarine::SDMABUFAttrs& attr return image; } +bool CHyprOpenGLImpl::attemptContextReset() { + Log::logger->log(Log::ERR, "GPU reset: attempting EGL context recovery..."); + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_eglContext) != EGL_TRUE) { + Log::logger->log(Log::ERR, "GPU reset: eglMakeCurrent failed, context may be lost"); + return false; + } + m_shadersInitialized = false; + Log::logger->log(Log::WARN, "GPU reset: EGL context re-established, shaders will reinitialize on next frame."); + return true; +} + void CHyprOpenGLImpl::beginSimple(PHLMONITOR pMonitor, const CRegion& damage, SP rb, SP fb) { g_pHyprRenderer->m_renderData.pMonitor = pMonitor; + if (m_gpuResetCooldown > 0) { + m_gpuResetCooldown--; + Log::logger->log(Log::WARN, "GPU reset recovery cooldown, skipping frame ({} remaining)", m_gpuResetCooldown); + g_pHyprRenderer->m_renderData.pMonitor.reset(); + return; + } + const GLenum RESETSTATUS = glGetGraphicsResetStatus(); if (RESETSTATUS != GL_NO_ERROR) { std::string errStr = ""; @@ -675,7 +694,14 @@ void CHyprOpenGLImpl::beginSimple(PHLMONITOR pMonitor, const CRegion& damage, SP case GL_UNKNOWN_CONTEXT_RESET: errStr = "GL_UNKNOWN_CONTEXT_RESET"; break; default: errStr = "UNKNOWN??"; break; } - RASSERT(false, "Aborting, glGetGraphicsResetStatus returned {}. Cannot continue until proper GPU reset handling is implemented.", errStr); + Log::logger->log(Log::ERR, "GPU reset detected in beginSimple: {}. Attempting EGL context recovery.", errStr); + if (!attemptContextReset()) { + Log::logger->log(Log::ERR, "GPU reset recovery failed. Skipping frames for cooldown."); + m_gpuResetCooldown = 60; + } else { + Log::logger->log(Log::WARN, "GPU reset recovery succeeded. Skipping current frame to reinitialize."); + } + g_pHyprRenderer->m_renderData.pMonitor.reset(); return; } @@ -714,6 +740,13 @@ void CHyprOpenGLImpl::makeEGLCurrent() { void CHyprOpenGLImpl::begin(PHLMONITOR pMonitor, const CRegion& damage_, SP fb, std::optional finalDamage) { g_pHyprRenderer->m_renderData.pMonitor = pMonitor; + if (m_gpuResetCooldown > 0) { + m_gpuResetCooldown--; + Log::logger->log(Log::WARN, "GPU reset recovery cooldown, skipping frame ({} remaining)", m_gpuResetCooldown); + g_pHyprRenderer->m_renderData.pMonitor.reset(); + return; + } + const GLenum RESETSTATUS = glGetGraphicsResetStatus(); if (RESETSTATUS != GL_NO_ERROR) { std::string errStr = ""; @@ -723,7 +756,14 @@ void CHyprOpenGLImpl::begin(PHLMONITOR pMonitor, const CRegion& damage_, SPlog(Log::ERR, "GPU reset detected in begin: {}. Attempting EGL context recovery.", errStr); + if (!attemptContextReset()) { + Log::logger->log(Log::ERR, "GPU reset recovery failed. Skipping frames for cooldown."); + m_gpuResetCooldown = 60; + } else { + Log::logger->log(Log::WARN, "GPU reset recovery succeeded. Skipping current frame to reinitialize."); + } + g_pHyprRenderer->m_renderData.pMonitor.reset(); return; } @@ -856,8 +896,10 @@ void CHyprOpenGLImpl::end() { // check for gl errors const GLenum ERR = glGetError(); - if UNLIKELY (ERR == GL_CONTEXT_LOST) /* We don't have infra to recover from this */ - RASSERT(false, "glGetError at Opengl::end() returned GL_CONTEXT_LOST. Cannot continue until proper GPU reset handling is implemented."); + if UNLIKELY (ERR == GL_CONTEXT_LOST) { + Log::logger->log(Log::ERR, "glGetError at Opengl::end() returned GL_CONTEXT_LOST. Recovery will trigger on next begin()."); + m_gpuResetCooldown = 60; + } } } @@ -1443,8 +1485,13 @@ WP CHyprOpenGLImpl::renderToFBInternal(SP tex, const STexture void CHyprOpenGLImpl::renderTextureInternal(SP tex, const CBox& box, const STextureRenderData& data) { RASSERT(g_pHyprRenderer->m_renderData.pMonitor, "Tried to render texture without begin()!"); - RASSERT(tex, "Attempted to draw nullptr texture!"); - RASSERT(tex->ok(), "Attempted to draw invalid texture!"); + + if UNLIKELY (!tex || !tex->ok()) { + // After a GPU reset, textures become invalid. Skip the draw + // instead of aborting — recovery will happen on the next begin(). + Log::logger->log(Log::ERR, "renderTextureInternal: invalid texture (likely GPU reset). Skipping draw."); + return; + } TRACY_GPU_ZONE("RenderTextureInternalWithDamage"); diff --git a/src/render/OpenGL.hpp b/src/render/OpenGL.hpp index 78518ffe7..c74cb5a24 100644 --- a/src/render/OpenGL.hpp +++ b/src/render/OpenGL.hpp @@ -313,6 +313,8 @@ namespace Render::GL { bool m_applyFinalShader = false; bool m_blend = false; bool m_offloadedFramebuffer = false; + int m_gpuResetCooldown = 0; + bool attemptContextReset(); bool m_cmSupported = true; SP m_finalScreenShader;