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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-02 18:19:21 +01:00
1 parent 9c9eefa358
commit 16544a3bd3
1 file changed
+9 -7
+9 -7
View File
@@ -250,15 +250,14 @@ void DisplayFireVblankEnd() {
}
void DisplayFireFlip() {
std::vector<FlipListener> toCall = [] {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> guard(listenersLock);
flipListeners.clear();
}
frameStartTicks = 0;
numVBlanks = 0;
isVblank = 0;