From 16544a3bd32249585124cb03fe834940887f3af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 17:56:18 +0100 Subject: [PATCH] Display: Don't call a flip listener after it's been removed DisplayFireFlip() copied the listener list and called the copy without the lock, so a debugger connection closing on its own thread could free its GPU stats subscriber while the CPU thread was about to call it. Headless crashed (or hung) in WebSocketGPUStatsState::FlipListener() with scripts that open a connection per command. Hold the lock across the calls, as LogManager does for its callbacks, so __DisplayForgetFlip() returns only once the listener can't be running. Co-Authored-By: Claude Opus 5.5 --- Core/HW/Display.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Core/HW/Display.cpp b/Core/HW/Display.cpp index f21d442d33..419e02162d 100644 --- a/Core/HW/Display.cpp +++ b/Core/HW/Display.cpp @@ -250,15 +250,14 @@ void DisplayFireVblankEnd() { } void DisplayFireFlip() { - std::vector toCall = [] { - std::lock_guard guard(listenersLock); - return flipListeners; - }(); - // This is also the right time to calculate FPS. CalculateFPS(); - for (FlipListener cb : toCall) { + // Held across the calls, so that once __DisplayForgetFlip() returns (on a debugger connection's + // thread, say) its listener isn't running and won't be called again. A listener must not add or + // remove listeners. + std::lock_guard guard(listenersLock); + for (const FlipListener &cb : flipListeners) { cb.first(cb.second); } } @@ -281,7 +280,10 @@ void __DisplayForgetFlip(FlipCallback callback, void *userdata) { // This is called on game bootup. void DisplayHWInit() { - flipListeners.clear(); + { + std::lock_guard guard(listenersLock); + flipListeners.clear(); + } frameStartTicks = 0; numVBlanks = 0; isVblank = 0;