diff --git a/Core/EmuThread.cpp b/Core/EmuThread.cpp index aed82b24f4..10aababd5f 100644 --- a/Core/EmuThread.cpp +++ b/Core/EmuThread.cpp @@ -13,7 +13,10 @@ #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" @@ -125,34 +128,68 @@ bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std return true; } +void RunGraphicsLoop(GraphicsContext *graphicsContext, Application *application, std::function frame, std::function 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 frame, std::string *errorMessage) { - // This is now the render thread, and will spawn the emu thread below. 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; } - if (graphicsContext->NeedsSeparateEmuThread()) { - SetCurrentThreadName("RenderThread"); - - g_inLoop = true; - std::thread emuThread = EmuThread_Start(graphicsContext, application, frame); - graphicsContext->ThreadStart(); - // This thread becomes the render thread. EmuThread will tell it when to quit by sending a message. - while (graphicsContext->ThreadFrame()) {} - EmuThread_Join(graphicsContext, emuThread); - g_inLoop = false; - - graphicsContext->ThreadEnd(); - - INFO_LOG(Log::System, "RenderThread - joined"); - - } else { - SetCurrentThreadName("MainThread"); - - RunMainLoop(graphicsContext, application, frame); - } + RunGraphicsLoop(graphicsContext, application, frame); graphicsContext->ShutdownSurface(); return true; } diff --git a/Core/EmuThread.h b/Core/EmuThread.h index b220b0c5fe..705421a774 100644 --- a/Core/EmuThread.h +++ b/Core/EmuThread.h @@ -50,3 +50,15 @@ void EmuThread_Join(GraphicsContext *graphicsContext, std::thread &emuThread); // Call from the main thread. // NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). bool RunMainLoop(GraphicsContext *graphicsContext, Application *application, std::function frame); + +// Runs whichever thread arrangement graphicsContext asks for, on the calling thread, and doesn't +// return until the emu thread is done. Either this thread becomes the render thread and the emu +// thread is spawned alongside it (OpenGL, which needs its API calls on the thread its context is +// current on), or this thread just runs the main loop because the backend spawns its own render +// thread (Vulkan, D3D11). +// The surface has to be initialized already, and is left alone - use MainThreadFunc if you want +// that taken care of too. +// shouldExit is optional, but required if you might run this in VR: see the comment on the render +// loop in EmuThread.cpp for why ThreadFrame() alone isn't enough to get out of there. +// NOTE: Does take ownership over Application (which is just a wrapper for NativeInitGraphics/NativeShutdownGraphics/NativeFrame). +void RunGraphicsLoop(GraphicsContext *graphicsContext, Application *application, std::function frame, std::function shouldExit = nullptr); diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index e034d6584a..492252290a 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -106,14 +106,6 @@ struct JNIEnv {}; #include "app-android.h" -enum class EmuThreadState { - DISABLED, - START_REQUESTED, - RUNNING, - QUIT_REQUESTED, - STOPPED, -}; - AndroidAudioState *g_audioState; struct FrameCommand { @@ -1558,18 +1550,13 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitRenderL exitRenderLoop = false; } -// TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread? // This function must release the wnd reference. static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContext) { - // The two backends split the work in opposite directions, which is why this thread has two - // names. Vulkan handles its own threading: this thread runs the main loop and the render - // manager spawns the actual render thread. OpenGL doesn't: this thread becomes the render - // thread and spawns the emu thread, which is what Java's GL thread used to do for us. - const bool separateEmuThread = graphicsContext->NeedsSeparateEmuThread(); - SetCurrentThreadName(separateEmuThread ? "RenderThread" : "EmuThread"); + // Only a provisional name, so the surface init below has something to log under. RunGraphicsLoop + // renames this thread once it knows which of the two roles it's going to take. + SetCurrentThreadName("RenderLoop"); AndroidJNIThreadContext ctx; - JNIEnv *env = getEnv(); _assert_(graphicsContext); if (exitRenderLoop) { @@ -1600,44 +1587,13 @@ static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContex ProcessFrameCommands(); return !exitRenderLoop; }; - if (separateEmuThread) { - // EmuThread_Start calls ThreadStart() for us, and EmuThread_Join calls ThreadEnd(), so - // neither is called directly here. - std::thread emuThread = EmuThread_Start(graphicsContext, new NativeApplication(), frame); + // Both backends share one lifecycle now, which is the point of all this: RunGraphicsLoop picks + // the thread arrangement the context needs and doesn't return until emulation is done. Windows + // and headless go through the same function, via MainThreadFunc. + // We have to hand it exitRenderLoop as well as the frame callback - in VR the loop can be + // parked waiting for the session to come back, where the callback is never reached. + RunGraphicsLoop(graphicsContext, new NativeApplication(), frame, []() { return exitRenderLoop.load(); }); - // The views were created against the previous surface, if there was one. - System_PostUIMessage(UIMessage::RECREATE_VIEWS); - - if (IsVREnabled()) { - // Only ever a GL thing, which is why it lives in this branch. - static bool vrFirstStart = true; - EnterVR(vrFirstStart); - vrFirstStart = false; - } - - // This is the GL "GPU thread" - the loop Java's GLSurfaceView used to drive one call at a - // time. ThreadFrame returns false once the emu thread is done producing. - while (!exitRenderLoop) { - if (IsVREnabled() && !StartVRRender()) { - // The session isn't active, so there's no frame to render into. Java's GL thread - // just missed a vsync here; we drive the loop ourselves, so pace it by hand - // rather than spinning. - 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 has queued, so it can't block on us. - EmuThread_Join(graphicsContext, emuThread); - } else { - RunMainLoop(graphicsContext, new NativeApplication(), frame); - } renderer_inited = false; // Shut the graphics context down to the same state it was in when we entered the render thread. diff --git a/docs/DebuggerThreading.md b/docs/DebuggerThreading.md index 2975476c35..23d7750641 100644 --- a/docs/DebuggerThreading.md +++ b/docs/DebuggerThreading.md @@ -34,11 +34,12 @@ mechanisms now exist for doing this safely - pick based on whether you're mutati Architecture fact that makes `g_frameMutex` correct: regardless of graphics backend, `NativeFrame()` (and thus CPU emulation via `Core_RunLoopUntil()`, and the Dear ImGui debugger) always runs on the -*same* thread - see `Core/EmuThread.cpp`. When a backend needs its own thread for actual graphics -API calls (`GraphicsContext::NeedsSeparateEmuThread()` - true for OpenGL, SDL, headless, libretro, -Qt; false for D3D11/Vulkan on Windows), the *original* thread stays behind purely to pump -`graphicsContext->ThreadFrame()` (i.e. just executes queued graphics API calls), and a *newly -spawned* thread takes over `NativeFrame()`/game logic/CPU duty. So `UI/ImDebugger/*.cpp` is always +*same* thread - see `RunGraphicsLoop()` in `Core/EmuThread.cpp`, which every platform goes through +(Windows and headless via `MainThreadFunc()`, Android from its render loop thread). When a backend +needs its own thread for actual graphics API calls (`GraphicsContext::NeedsSeparateEmuThread()` - +true for OpenGL, SDL, headless, libretro, Qt; false for D3D11/Vulkan on Windows), the *original* +thread stays behind purely to pump `graphicsContext->ThreadFrame()` (i.e. just executes queued +graphics API calls), and a *newly spawned* thread takes over `NativeFrame()`/game logic/CPU duty. So `UI/ImDebugger/*.cpp` is always safe to read/write this state directly, without either mechanism - it's always on the same thread as `Core_RunLoopUntil()`. The legacy Win32 debugger is different: its dialogs are pumped by the *original* `WinMain` message-loop thread, which is a genuinely separate OS thread from whichever