Files
ppsspp/Core/EmuThread.cpp
T
Henrik RydgårdandClaude Opus 5 bd94e8141f Android: Converge the GL and Vulkan lifecycles on a shared RunGraphicsLoop
Stage 5 of the JavaGL removal. Now that the GL path owns its own EGL context and
render thread, Android's two lifecycle branches and the Win32 one in
MainThreadFunc were the same code three times over, so this moves the branch into
one place - RunGraphicsLoop() in Core/EmuThread.cpp. It picks the thread
arrangement the context asks for: either this thread becomes the render thread
and spawns the emu thread (OpenGL), or it just runs the main loop because the
backend spawns its own render thread (Vulkan, D3D11). Android, Windows and
headless all go through it now, which is what the "TODO: Merge with the Win32
EmuThread and so on" in app-android.cpp was asking for.

Falls out of having one implementation instead of three:

- MainThreadFunc called graphicsContext->ThreadStart() right after
  EmuThread_Start(), which already calls it, and ThreadEnd() right after
  EmuThread_Join(), which already calls that too. So Windows and headless GL ran
  CreateDeviceObjects()/DestroyDeviceObjects() twice per context, leaking a VAO
  on the way in and double-deleting it on the way out. Dates back to 16206d18ab,
  which moved those calls into EmuThread_Start/Join without removing the
  originals.

- RECREATE_VIEWS is gone. Only the GL path ever posted it; Vulkan goes through
  the same destroy/recreate without it and always has. Verified on device: views
  survive pause/resume, and both a real rotation and a density change - each one
  an activity destroy and recreate - lay them out again by themselves.

- The non-separate path is now named EmuThread rather than MainThread on
  Windows. It's the thread that runs NativeFrame(), which is what
  DebuggerThreading.md calls the emu thread, and the real main thread there is
  the GUI message pump.

The loop keeps an explicit exit flag, as a shouldExit callback, rather than
relying on ThreadFrame() returning false. In VR we skip ThreadFrame() entirely
while the XR session is idle, and the session going idle is exactly what
backgrounding the app does - so the exit would never be observed, the render
thread would sit in that sleep forever, and the UI thread joining it would hang.
That is not hypothetical: it deadlocked a Quest 2 on the first try.

Also drops a dead EmuThreadState enum and an unused JNIEnv local in
app-android.cpp, both orphaned when the JavaGL JNI entry points went away.

Tested on a nubia Z2464N (Mali-G57, Android 15) on both backends: cold start,
booting a homebrew, pause/resume in the menu, in a settings screen and in-game,
rotation back and forth, a density-change activity recreate with the game live,
the pause menu, and exit to menu. ThreadStart/ThreadEnd log exactly once per
context, and every one of those cycles tears the EGL context down and builds a
new one.

Also tested on a Quest 2 with the vr flavor, which is what shook out the deadlock
above: cold start into a focused XR session at 72fps, backgrounding (render
thread joins in 27ms where it used to never return), resuming back into VR with
a fresh EGL context, and exiting from in-app. xrCreateSession runs on the render
loop thread, which is where the EGL context is current - VRBase.cpp builds its
graphics binding from eglGetCurrentDisplay/eglGetCurrentContext, so that ordering
matters.

Windows x64 debug and the Android gradle builds are clean.

Not converged, and not a small change: NeedsSeparateEmuThread() itself. GL needs
its API calls on the thread its context is current on, so somebody has to pump
ThreadFrame() there, whereas VulkanRenderManager owns its thread outright.
Getting rid of the virtual means teaching GLRenderManager to spawn and own its
own render thread, for every GL platform at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:29:12 +02:00

196 lines
6.8 KiB
C++

#include "ppsspp_config.h"
#include <mutex>
#include <atomic>
#include <thread>
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/System/Application.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Input/InputState.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/GPU/GraphicsContext.h"
#include "Common/System/Display.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/TimeUtil.h"
#include "Common/VR/PPSSPPVR.h"
#include "Core/EmuThread.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/Config.h"
#include "Core/ConfigValues.h"
enum class EmuThreadState {
RUNNING,
QUIT_REQUESTED,
STOPPED,
};
static std::atomic<EmuThreadState> g_emuThreadState(EmuThreadState::STOPPED);
static std::atomic<bool> g_inLoop;
class GraphicsContext;
bool MainThread_Ready() {
return g_inLoop;
}
static void EmuThreadFunc(GraphicsContext *graphicsContext, Application *application, std::function<bool (GraphicsContext *)> frame) {
INFO_LOG(Log::G3D, "Entering separate emu thread");
SetCurrentThreadName("EmuThread");
g_emuThreadState = EmuThreadState::RUNNING;
AndroidJNIThreadContext context;
// This normally calls NativeInitGraphics()
if (!application->InitGraphics(graphicsContext)) {
_assert_msg_(false, "NativeInitGraphics failed, might as well bail");
// If this fails, which it normally shouldn't, let's bail.
g_emuThreadState = EmuThreadState::QUIT_REQUESTED;
} else {
INFO_LOG(Log::G3D, "EmuThread: Entering loop");
}
while (g_emuThreadState != EmuThreadState::QUIT_REQUESTED) {
// We're here again, so the game quit. Restart Run() which controls the UI.
// This way they can load a new game.
// This normally calls NativeFrame()
if (!frame(graphicsContext)) {
g_emuThreadState = EmuThreadState::QUIT_REQUESTED;
}
}
INFO_LOG(Log::System, "emuThreadState was set to QUIT_REQUESTED, left EmuThreadFunc loop. Setting state to STOPPED.");
// This normally calls NativeShutdownGraphics()
application->ShutdownGraphics(graphicsContext);
delete application;
INFO_LOG(Log::System, "Leaving separate emu thread");
g_emuThreadState = EmuThreadState::STOPPED;
}
std::thread EmuThread_Start(GraphicsContext *graphicsContext, Application *application, std::function<bool(GraphicsContext *)> frame) {
INFO_LOG(Log::System, "EmuTread_Start");
_dbg_assert_(g_emuThreadState == EmuThreadState::STOPPED);
std::thread emuThread = std::thread(&EmuThreadFunc, graphicsContext, application, frame);
graphicsContext->ThreadStart();
return emuThread;
}
// This is useful when the render thread is in control.
void EmuThread_RequestExit() {
INFO_LOG(Log::System, "EmuTread_RequestExit");
if (g_emuThreadState == EmuThreadState::RUNNING) {
g_emuThreadState = EmuThreadState::QUIT_REQUESTED;
} else {
INFO_LOG(Log::System, "EmuTread_RequestExit: g_emuThreadState was not RUNNING, so not requesting exit.");
}
}
void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread) {
INFO_LOG(Log::System, "EmuTread_Join");
if (graphicsContext->NeedsSeparateEmuThread()) {
EmuThread_RequestExit();
while (graphicsContext->ThreadFrame()) {}
}
emuThread.join();
emuThread = std::thread();
graphicsContext->ThreadEnd();
}
bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std::function<bool(GraphicsContext *)> frame) {
// This is the main loop for graphics context that handle their own threading.
// InitFromRenderThread/ShutdownFromRenderThread are not used.
application->InitGraphics(graphicsContext);
g_inLoop = true;
while (frame(graphicsContext)) {}
// NOTE: Don't call stuff like Core_Stop here. On Android, we fully shut down graphics when you switch away from the app,
// then boot it up again when returning. That means stopping this thread and restarting it.
// Process the shutdown. Without this, non-GL delays 800ms on shutdown. TODO: is this still an issue?
Core_StateProcessed();
g_inLoop = false;
application->ShutdownGraphics(graphicsContext);
delete application;
return true;
}
void RunGraphicsLoop(GraphicsContext *graphicsContext, Application *application, std::function<bool(GraphicsContext *)> frame, std::function<bool()> shouldExit) {
if (!graphicsContext->NeedsSeparateEmuThread()) {
// The backend spawns its own render thread, so this thread is simply where emulation runs.
SetCurrentThreadName("EmuThread");
RunMainLoop(graphicsContext, application, frame);
return;
}
// OpenGL wants its API calls on the thread its context is current on, which is this one. So
// this thread becomes the render thread - all it does is execute the GPU commands the emu
// thread queues up - and emulation moves to a thread we spawn here.
SetCurrentThreadName("RenderThread");
g_inLoop = true;
// EmuThread_Start calls ThreadStart() for us, and EmuThread_Join calls ThreadEnd(), so neither
// is called directly here.
std::thread emuThread = EmuThread_Start(graphicsContext, application, frame);
if (IsVREnabled()) {
static bool vrFirstStart = true;
EnterVR(vrFirstStart);
vrFirstStart = false;
}
// Normally ThreadFrame() returning false is what gets us out of here - the frame callback
// decides emulation is done, and the emu thread's NotifyEmuThreadExit then queues the exit.
// That isn't enough in VR: while the session is idle we skip ThreadFrame() entirely, and going
// to the background is precisely when the session goes idle, so the exit would never be seen
// and whoever is waiting to join this thread would wait forever. Hence shouldExit.
while (!(shouldExit && shouldExit())) {
if (IsVREnabled() && !StartVRRender()) {
// The session isn't active, so there's no frame to render into. Nothing to do until it
// comes back - wait out a frame rather than spinning on it.
sleep_ms(16, "vr-session-idle");
continue;
}
if (!graphicsContext->ThreadFrame()) {
break;
}
if (IsVREnabled()) {
UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y);
FinishVRRender();
}
}
// Also drains whatever the emu thread still had queued, so it can't get stuck waiting on us.
EmuThread_Join(graphicsContext, emuThread);
g_inLoop = false;
INFO_LOG(Log::System, "RenderThread - joined");
}
// Call InitAPI and ShutdownAPI outside this!
bool MainThreadFunc(GraphicsContext *graphicsContext, Application *application, const WindowDesc &windowDesc, std::function<bool(GraphicsContext *)> frame, std::string *errorMessage) {
if (!graphicsContext->InitSurface(windowDesc.winsys, windowDesc.data1, windowDesc.data2, errorMessage)) {
ERROR_LOG(Log::G3D, "MainThreadFunc: InitSurface failed: %s", errorMessage->c_str());
delete application;
return false;
}
RunGraphicsLoop(graphicsContext, application, frame);
graphicsContext->ShutdownSurface();
return true;
}