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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Fable 5.1 committed 2026-10-08 00:09:09 +02:00
1 parent 7641514084
commit 3cb6ada66a
4 files changed
+18 -1

No files matched your search

+4
View File
@@ -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() {}
+3 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <atomic>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -930,7 +931,8 @@ private:
bool syncDone_ = false;
GLDeleter deleter_;
bool skipGLCalls_ = false;
// Set from either thread, read on the render thread.
std::atomic<bool> skipGLCalls_{false};
int curFrame_ = 0;
@@ -33,6 +33,12 @@ public:
renderManager_->ThreadEnd();
}
void NotifyContextLost() override {
if (renderManager_) {
renderManager_->SetSkipGLCalls();
}
}
// Call from emu thread
void NotifyEmuThreadExit() override {
renderManager_->SetSkipGLCalls();
+5
View File
@@ -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();