From 3cb6ada66a6013ee9ff46ee0fbb277b19842082b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 21 Sep 2026 14:15:31 -0600 Subject: [PATCH] Android GL: Don't run stale GL commands when joining the emu thread EmuThread_Join drains the render queue on the calling thread. On Android that is either the UI thread with no GL context (NativeApp.shutdown), or the GL thread with a brand new context after the old one was lost (displayInit the second time). The skip flag was only set once the emu thread reached NativeShutdownGraphics, so everything queued before that ran for real, against nothing or against the wrong context. Add GraphicsContext::NotifyContextLost() and call it before the join in both places. skipGLCalls_ is now atomic, since it's set from one thread and read from another. Co-Authored-By: Claude Fable 5.1 --- Common/GPU/GraphicsContext.h | 4 ++++ Common/GPU/OpenGL/GLRenderManager.h | 4 +++- Common/GPU/OpenGL/OpenGLGraphicsContext.h | 6 ++++++ android/jni/app-android.cpp | 5 +++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Common/GPU/GraphicsContext.h b/Common/GPU/GraphicsContext.h index a4b2667a41..ca05a20a17 100644 --- a/Common/GPU/GraphicsContext.h +++ b/Common/GPU/GraphicsContext.h @@ -54,6 +54,10 @@ public: // Called from the emu thread, so be ready for that. virtual void NotifyEmuThreadExit() {} + // The API context that queued-up commands were recorded against is gone (or isn't current on the + // thread about to drain them), so they must not be executed for real. Only meaningful on Android GL. + virtual void NotifyContextLost() {} + // Useful for checks that need to be performed every frame. // Should strive to get rid of these. virtual void Poll() {} diff --git a/Common/GPU/OpenGL/GLRenderManager.h b/Common/GPU/OpenGL/GLRenderManager.h index edfe69b373..9c9f13f70a 100644 --- a/Common/GPU/OpenGL/GLRenderManager.h +++ b/Common/GPU/OpenGL/GLRenderManager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -930,7 +931,8 @@ private: bool syncDone_ = false; GLDeleter deleter_; - bool skipGLCalls_ = false; + // Set from either thread, read on the render thread. + std::atomic skipGLCalls_{false}; int curFrame_ = 0; diff --git a/Common/GPU/OpenGL/OpenGLGraphicsContext.h b/Common/GPU/OpenGL/OpenGLGraphicsContext.h index 88ba27f5f3..082ac4a819 100644 --- a/Common/GPU/OpenGL/OpenGLGraphicsContext.h +++ b/Common/GPU/OpenGL/OpenGLGraphicsContext.h @@ -33,6 +33,12 @@ public: renderManager_->ThreadEnd(); } + void NotifyContextLost() override { + if (renderManager_) { + renderManager_->SetSkipGLCalls(); + } + } + // Call from emu thread void NotifyEmuThreadExit() override { renderManager_->SetSkipGLCalls(); diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 50cb99e2b2..65522d3bd6 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -899,6 +899,8 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { if (renderer_inited && graphicsContext && graphicsContext->NeedsSeparateEmuThread()) { // Only used in Java EGL path. + // The join drains the render queue right here, on a thread without a GL context. + graphicsContext->NotifyContextLost(); INFO_LOG(Log::System, "Joining emuthread."); EmuThread_Join(graphicsContext, g_emuThread); @@ -967,6 +969,9 @@ extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * e // but the only mechanism for handling lost devices seems to be that onSurfaceCreated is called again, // which ends up calling displayInit. INFO_LOG(Log::G3D, "NativeApp.displayInit(): Second time, joining the emuthread and starting it up again."); + // We only get here with a fresh EGL context. What's left in the render queue refers to objects + // from the old one, so it mustn't be run against this one when the join drains it. + graphicsContext->NotifyContextLost(); EmuThread_Join(graphicsContext, g_emuThread); graphicsContext->ShutdownSurface();