diff --git a/CMakeLists.txt b/CMakeLists.txt index 291b570fe6..adfa8278a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -925,6 +925,8 @@ if(ANDROID) android/jni/app-android.cpp android/jni/AndroidAudio.cpp android/jni/AndroidAudio.h + android/jni/AndroidEGLGraphicsContext.cpp + android/jni/AndroidEGLGraphicsContext.h android/jni/OpenSLContext.cpp android/jni/OpenSLContext.h ) diff --git a/Common/GPU/OpenGL/OpenGLGraphicsContext.h b/Common/GPU/OpenGL/OpenGLGraphicsContext.h index 082ac4a819..2214830045 100644 --- a/Common/GPU/OpenGL/OpenGLGraphicsContext.h +++ b/Common/GPU/OpenGL/OpenGLGraphicsContext.h @@ -45,7 +45,9 @@ public: renderManager_->NotifyEmuThreadExit(); } -private: +protected: + // Protected rather than private so a subclass that owns the real context (see + // android/jni/AndroidEGLGraphicsContext.h) can hook up a swap function. Draw::DrawContext *draw_ = nullptr; GLRenderManager *renderManager_ = nullptr; }; diff --git a/Core/Config.cpp b/Core/Config.cpp index a82c9cc8c5..df0a205302 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -704,6 +704,7 @@ static const ConfigSetting graphicsSettings[] = { ConfigSetting("Smart2DTexFiltering", SETTING(g_Config, bSmart2DTexFiltering), false, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("InternalResolution", SETTING(g_Config, iInternalResolution), &DefaultInternalResolution, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("AndroidHwScale", SETTING(g_Config, iAndroidHwScale), 0, CfgFlag::DEFAULT), + ConfigSetting("AndroidNativeEGL", SETTING(g_Config, bAndroidNativeEGL), false, CfgFlag::DEFAULT), ConfigSetting("HighQualityDepth", SETTING(g_Config, bHighQualityDepth), true, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("FrameSkip", SETTING(g_Config, iFrameSkip), 0, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("AutoFrameSkip", SETTING(g_Config, bAutoFrameSkip), IsVREnabled(), CfgFlag::PER_GAME | CfgFlag::REPORT), diff --git a/Core/Config.h b/Core/Config.h index ca21fb0658..b369dadc5e 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -569,6 +569,12 @@ public: // Use the hardware scaler to scale up the image to save fillrate. Similar to Windows' window size, really. int iAndroidHwScale; // 0 = device resolution. 1 = 480x272 (extended to correct aspect), 2 = 960x544 etc. + // Android, OpenGL backend only: own EGL and the render thread in C++ instead of letting + // Java's GLSurfaceView do it. Temporary, while the new path is being shaken out on devices - + // the intent is for it to become the only way and for the JavaGL path to go away. + // Only read when the activity is created, so changing it needs an app restart. + bool bAndroidNativeEGL; + // Risky JIT optimizations bool bDiscardRegsOnJRRA; diff --git a/UI/DeveloperToolsScreen.cpp b/UI/DeveloperToolsScreen.cpp index c570b671ae..5a46cc1ba0 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -539,6 +539,17 @@ void DeveloperToolsScreen::CreateGraphicsTab(UI::LinearLayout *list) { }); } +#if PPSSPP_PLATFORM(ANDROID) + if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) { + list->Add(new ItemHeader(dev->T("OpenGL"))); + // Temporary, while the EGL path is being tested on devices - see Core/Config.h. + list->Add(new CheckBox(&g_Config.bAndroidNativeEGL, dev->T("Use native EGL instead of GLSurfaceView"), ""))->OnClick.Add([](UI::EventParams &e) { + // TODO: Not translating yet, same as the multi-threaded rendering checkbox above. + g_OSD.Show(OSDType::MESSAGE_WARNING, "Restart required"); + }); + } +#endif + if (GetGPUBackend() == GPUBackend::VULKAN && SupportsCustomDriver()) { auto driverChoice = list->Add(new Choice(gr->T("AdrenoTools driver manager"))); driverChoice->OnClick.Add([=](UI::EventParams &e) { diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 7147d4b93f..c6a3fa5fe0 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -940,6 +940,7 @@ LOCAL_MODULE := ppsspp_jni LOCAL_SRC_FILES := \ $(SRC)/android/jni/app-android.cpp \ $(SRC)/android/jni/AndroidAudio.cpp \ + $(SRC)/android/jni/AndroidEGLGraphicsContext.cpp \ $(SRC)/android/jni/OpenSLContext.cpp \ $(SRC)/UI/ImDebugger/ImDebugger.cpp \ $(SRC)/UI/ImDebugger/ImGe.cpp \ diff --git a/android/jni/AndroidEGLGraphicsContext.cpp b/android/jni/AndroidEGLGraphicsContext.cpp new file mode 100644 index 0000000000..6bc5d2d533 --- /dev/null +++ b/android/jni/AndroidEGLGraphicsContext.cpp @@ -0,0 +1,239 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include + +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Common/VR/PPSSPPVR.h" + +#include "android/jni/AndroidEGLGraphicsContext.h" + +// EGL_OPENGL_ES3_BIT_KHR, from EGL_KHR_create_context. Not in every platform header. +#ifndef EGL_OPENGL_ES3_BIT_KHR +#define EGL_OPENGL_ES3_BIT_KHR 0x0040 +#endif + +namespace { + +const char *EGLErrorToString(EGLint error) { + switch (error) { + case EGL_SUCCESS: return "EGL_SUCCESS"; + case EGL_NOT_INITIALIZED: return "EGL_NOT_INITIALIZED"; + case EGL_BAD_ACCESS: return "EGL_BAD_ACCESS"; + case EGL_BAD_ALLOC: return "EGL_BAD_ALLOC"; + case EGL_BAD_ATTRIBUTE: return "EGL_BAD_ATTRIBUTE"; + case EGL_BAD_CONTEXT: return "EGL_BAD_CONTEXT"; + case EGL_BAD_CONFIG: return "EGL_BAD_CONFIG"; + case EGL_BAD_CURRENT_SURFACE: return "EGL_BAD_CURRENT_SURFACE"; + case EGL_BAD_DISPLAY: return "EGL_BAD_DISPLAY"; + case EGL_BAD_SURFACE: return "EGL_BAD_SURFACE"; + case EGL_BAD_MATCH: return "EGL_BAD_MATCH"; + case EGL_BAD_PARAMETER: return "EGL_BAD_PARAMETER"; + case EGL_BAD_NATIVE_PIXMAP: return "EGL_BAD_NATIVE_PIXMAP"; + case EGL_BAD_NATIVE_WINDOW: return "EGL_BAD_NATIVE_WINDOW"; + case EGL_CONTEXT_LOST: return "EGL_CONTEXT_LOST"; + default: return "(unknown EGL error)"; + } +} + +std::string EGLFailure(const char *what) { + const EGLint error = eglGetError(); + return StringFromFormat("%s failed: %s (%04x)", what, EGLErrorToString(error), error); +} + +struct ConfigAttempt { + const char *desc; + EGLint red, green, blue, alpha, depth, stencil; +}; + +// Most to least desirable. The PSP only needs 16 bits of depth, but ask for 24 first - it is what +// essentially every driver offers anyway, and asking for less does not make a 16-bit buffer appear. +// Stencil is not optional in practice: without it, non-buffered rendering has nothing to work with. +// +// The last entry is the fallback: ask for the bare minimum and take whatever eglChooseConfig puts +// first, which is roughly what GLSurfaceView's default chooser does today. +const ConfigAttempt kConfigAttempts[] = { + { "RGBA8888 D24S8", 8, 8, 8, 8, 24, 8 }, + { "RGBA8888 D16S8", 8, 8, 8, 8, 16, 8 }, + { "RGB565 D16S8", 5, 6, 5, 0, 16, 8 }, + { "anything", 5, 6, 5, 0, 16, 0 }, +}; + +} // namespace + +bool AndroidEGLGraphicsContext::ChooseConfig(bool wantGLES3, std::string *errorMessage) { + const EGLint renderableType = wantGLES3 ? EGL_OPENGL_ES3_BIT_KHR : EGL_OPENGL_ES2_BIT; + + for (const ConfigAttempt &attempt : kConfigAttempts) { + const EGLint attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, renderableType, + EGL_RED_SIZE, attempt.red, + EGL_GREEN_SIZE, attempt.green, + EGL_BLUE_SIZE, attempt.blue, + EGL_ALPHA_SIZE, attempt.alpha, + EGL_DEPTH_SIZE, attempt.depth, + EGL_STENCIL_SIZE, attempt.stencil, + EGL_NONE + }; + EGLConfig config = nullptr; + EGLint numConfigs = 0; + if (eglChooseConfig(display_, attribs, &config, 1, &numConfigs) && numConfigs > 0) { + config_ = config; + EGLint red = 0, green = 0, blue = 0, alpha = 0, depth = 0, stencil = 0, samples = 0; + eglGetConfigAttrib(display_, config_, EGL_RED_SIZE, &red); + eglGetConfigAttrib(display_, config_, EGL_GREEN_SIZE, &green); + eglGetConfigAttrib(display_, config_, EGL_BLUE_SIZE, &blue); + eglGetConfigAttrib(display_, config_, EGL_ALPHA_SIZE, &alpha); + eglGetConfigAttrib(display_, config_, EGL_DEPTH_SIZE, &depth); + eglGetConfigAttrib(display_, config_, EGL_STENCIL_SIZE, &stencil); + eglGetConfigAttrib(display_, config_, EGL_SAMPLES, &samples); + INFO_LOG(Log::G3D, "EGL config (asked for %s): R%dG%dB%dA%d depth=%d stencil=%d samples=%d", + attempt.desc, red, green, blue, alpha, depth, stencil, samples); + if (stencil == 0) { + WARN_LOG(Log::G3D, "EGL config has no stencil buffer - non-buffered rendering will misbehave."); + } + return true; + } + INFO_LOG(Log::G3D, "No EGL config for %s, trying the next one", attempt.desc); + } + + *errorMessage = "Found no usable EGL config"; + return false; +} + +bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) { + _dbg_assert_(winsys == WINDOWSYSTEM_ANDROID); + wnd_ = (ANativeWindow *)data1; + if (!wnd_) { + *errorMessage = "AndroidEGLGraphicsContext::InitSurface: no native window"; + return false; + } + + display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (display_ == EGL_NO_DISPLAY) { + *errorMessage = EGLFailure("eglGetDisplay"); + return false; + } + EGLint eglMajor = 0, eglMinor = 0; + if (!eglInitialize(display_, &eglMajor, &eglMinor)) { + *errorMessage = EGLFailure("eglInitialize"); + display_ = EGL_NO_DISPLAY; + return false; + } + INFO_LOG(Log::G3D, "EGL %d.%d initialized (vendor: %s)", eglMajor, eglMinor, + eglQueryString(display_, EGL_VENDOR)); + + // Mirrors NativeRenderer's setEGLContextClientVersion(isVRDevice() ? 3 : 2). Asking for 2 is + // not a cap: GLES3 is detected from GL_VERSION at runtime (see GLFeatures.cpp), which is how + // the Java path has always ended up using GLES3 on devices that have it. + const bool wantGLES3 = IsVREnabled(); + if (!ChooseConfig(wantGLES3, errorMessage)) { + DestroyEGL(); + return false; + } + + // Line the window's pixel format up with the config we picked. Skipping this is how you get + // EGL_BAD_MATCH out of eglCreateWindowSurface on drivers that take the window format + // literally - historically the reason the Java path asked for no particular config at all. + // Width and height stay 0 so whatever size Java set (see iAndroidHwScale) is left alone. + EGLint nativeVisualID = 0; + if (eglGetConfigAttrib(display_, config_, EGL_NATIVE_VISUAL_ID, &nativeVisualID)) { + ANativeWindow_setBuffersGeometry(wnd_, 0, 0, nativeVisualID); + } else { + WARN_LOG(Log::G3D, "Couldn't get EGL_NATIVE_VISUAL_ID, leaving the window format alone"); + } + + const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, wantGLES3 ? 3 : 2, EGL_NONE }; + context_ = eglCreateContext(display_, config_, EGL_NO_CONTEXT, contextAttribs); + if (context_ == EGL_NO_CONTEXT) { + *errorMessage = EGLFailure("eglCreateContext"); + DestroyEGL(); + return false; + } + + surface_ = eglCreateWindowSurface(display_, config_, wnd_, nullptr); + if (surface_ == EGL_NO_SURFACE) { + *errorMessage = EGLFailure("eglCreateWindowSurface"); + DestroyEGL(); + return false; + } + + if (!eglMakeCurrent(display_, surface_, surface_, context_)) { + *errorMessage = EGLFailure("eglMakeCurrent"); + DestroyEGL(); + return false; + } + + EGLint surfaceWidth = 0, surfaceHeight = 0; + eglQuerySurface(display_, surface_, EGL_WIDTH, &surfaceWidth); + eglQuerySurface(display_, surface_, EGL_HEIGHT, &surfaceHeight); + INFO_LOG(Log::G3D, "EGL context current, surface is %dx%d", surfaceWidth, surfaceHeight); + + // GL is current on this thread now, so the shared setup (extension check, thin3d context, + // render manager) can run. + if (!OpenGLGraphicsContext::InitSurface(winsys, data1, data2, errorMessage)) { + DestroyEGL(); + return false; + } + + renderManager_->SetSwapFunction([this]() { + if (!eglSwapBuffers(display_, surface_)) { + // Most likely the surface went away under us. The render loop is about to be asked to + // exit anyway, so just complain rather than trying to recover from here. + WARN_LOG(Log::G3D, "%s", EGLFailure("eglSwapBuffers").c_str()); + } + }); + + // NOTE: Deliberately *not* calling SetSwapIntervalFunction. GLSurfaceView gave us no way to + // change the interval, so PresentMode::IMMEDIATE has always been a no-op here, and honoring it + // now would be a behavior change rather than a port. Worth revisiting once this path is the + // only one. + + return true; +} + +void AndroidEGLGraphicsContext::ShutdownSurface() { + // Order matters: the render manager's teardown still issues GL calls, so the context has to + // stay current until it's done. + OpenGLGraphicsContext::ShutdownSurface(); + DestroyEGL(); +} + +void AndroidEGLGraphicsContext::DestroyEGL() { + if (display_ == EGL_NO_DISPLAY) { + return; + } + INFO_LOG(Log::G3D, "AndroidEGLGraphicsContext::DestroyEGL"); + eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (surface_ != EGL_NO_SURFACE) { + eglDestroySurface(display_, surface_); + surface_ = EGL_NO_SURFACE; + } + if (context_ != EGL_NO_CONTEXT) { + eglDestroyContext(display_, context_); + context_ = EGL_NO_CONTEXT; + } + // eglTerminate only tears down this thread's binding of the display, which is refcounted per + // process - so this is safe even though the display is shared. + eglTerminate(display_); + eglReleaseThread(); + display_ = EGL_NO_DISPLAY; + config_ = nullptr; + wnd_ = nullptr; +} diff --git a/android/jni/AndroidEGLGraphicsContext.h b/android/jni/AndroidEGLGraphicsContext.h new file mode 100644 index 0000000000..27997ec315 --- /dev/null +++ b/android/jni/AndroidEGLGraphicsContext.h @@ -0,0 +1,61 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +#include "Common/GPU/OpenGL/OpenGLGraphicsContext.h" + +struct ANativeWindow; + +// Owns EGL for the OpenGL backend on Android, instead of letting Java's GLSurfaceView own it +// (NativeRenderer.java / NativeGLSurfaceView.java, the "JavaGL" path). +// +// The point is not EGL itself but who owns the render thread. With GLSurfaceView, Java creates +// the thread and the context and calls us back, so a lost context is only ever observable after +// the fact, as a second onSurfaceCreated - which is what forces app-android.cpp's displayInit to +// tear down and restart the emu thread from inside a JNI callback. Owning EGL here lets the GL +// backend use the same lifecycle as Vulkan: we start a render thread when we get a surface and +// join it when the surface goes away, which is the whole reason this exists. +// +// Deliberately behaves the same as the Java path otherwise - same GLES client version, same +// window pixel format, same separate emu thread. See Core/Config.h, bAndroidNativeEGL. +class AndroidEGLGraphicsContext : public OpenGLGraphicsContext { +public: + // Note: not just DestroyEGL(). The base destructor deletes the thin3d context, which still + // makes GL calls, so EGL has to outlive it - ShutdownSurface() gets that order right, and is + // a no-op if the render loop already called it. + ~AndroidEGLGraphicsContext() override { ShutdownSurface(); } + + // Must be called on the thread that will become the render thread - the GL context ends up + // current on the calling thread. data1 is the ANativeWindow, as on the Vulkan path. + bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override; + void ShutdownSurface() override; + +private: + // Picks a config, from the most to the least desirable. Fills in config_. + bool ChooseConfig(bool wantGLES3, std::string *errorMessage); + void DestroyEGL(); + + EGLDisplay display_ = EGL_NO_DISPLAY; + EGLSurface surface_ = EGL_NO_SURFACE; + EGLContext context_ = EGL_NO_CONTEXT; + EGLConfig config_ = nullptr; + // Not owned - the caller releases it after the render loop exits. + ANativeWindow *wnd_ = nullptr; +}; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 65522d3bd6..458dfa5506 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -89,6 +89,7 @@ struct JNIEnv {}; #include "Common/GPU/GraphicsContext.h" #include "Common/GPU/Vulkan/VulkanGraphicsContext.h" #include "Common/GPU/OpenGL/OpenGLGraphicsContext.h" +#include "android/jni/AndroidEGLGraphicsContext.h" #include "Common/StringUtils.h" #include "Common/TimeUtil.h" @@ -576,6 +577,11 @@ static std::string QueryConfig(std::string_view query) { if (GetGPUBackend() == GPUBackend::VULKAN) { return "false"; } + // GL with our own EGL wants the same treatment as Vulkan: a plain SurfaceView, and a + // render thread we start ourselves once there's a surface. + if (g_Config.bAndroidNativeEGL) { + return "false"; + } // Otherwise, some devices prefer the Java init so play it safe. return "true"; } else if (query == "audioMixWithOthers") { @@ -774,9 +780,15 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init retry: switch (g_Config.iGPUBackend) { case (int)GPUBackend::OPENGL: - INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)"); - graphicsContext = new OpenGLGraphicsContext(); - INFO_LOG(Log::System, "NativeApp.init() - not yet launching emuthread, waiting to displayInit"); + if (g_Config.bAndroidNativeEGL) { + INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (native EGL)"); + graphicsContext = new AndroidEGLGraphicsContext(); + // Like Vulkan, we wait for a surface before doing anything - see runRenderLoop. + } else { + INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context (JavaGL)"); + graphicsContext = new OpenGLGraphicsContext(); + INFO_LOG(Log::System, "NativeApp.init() - not yet launching emuthread, waiting to displayInit"); + } break; case (int)GPUBackend::VULKAN: { @@ -897,8 +909,10 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_pause(JNIEnv *, jclass) { extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { INFO_LOG(Log::System, "NativeApp.shutdown() -- begin"); - if (renderer_inited && graphicsContext && graphicsContext->NeedsSeparateEmuThread()) { - // Only used in Java EGL path. + // Only the JavaGL path gets here with a thread to join: it's the one that starts the emu thread + // from a JNI callback and leaves it in g_emuThread. On the native-EGL and Vulkan paths the + // render loop thread owns its own, and has already been joined via requestExitRenderLoop. + if (renderer_inited && graphicsContext && graphicsContext->NeedsSeparateEmuThread() && g_emuThread.joinable()) { // The join drains the render queue right here, on a thread without a GL context. graphicsContext->NotifyContextLost(); INFO_LOG(Log::System, "Joining emuthread."); @@ -1633,25 +1647,24 @@ static void ProcessFrameCommands() { std::thread g_renderLoopThread; -static void VulkanEmuThread(ANativeWindow *wnd, GraphicsContext *graphicsContext); +static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContext); -// This runs in Vulkan mode only. -// This handles the entire lifecycle of the Vulkan context, init and exit. -extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_runVulkanRenderLoop(JNIEnv * env, jobject obj, jobject _surf) { +// Runs on the Vulkan path, and on the OpenGL path when we own EGL (bAndroidNativeEGL). Not in +// JavaGL mode - there, Java owns the thread and calls displayInit/displayRender instead. +// This handles the entire lifecycle of the graphics context's surface, init and exit. +extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_runRenderLoop(JNIEnv * env, jobject obj, jobject _surf) { if (!graphicsContext) { - ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Tried to enter without a created graphics context."); + ERROR_LOG(Log::G3D, "runRenderLoop: Tried to enter without a created graphics context."); return false; } - _assert_(!graphicsContext->NeedsSeparateEmuThread()); - if (g_renderLoopThread.joinable()) { if (renderLoopRunning) { - ERROR_LOG(Log::G3D, "runVulkanRenderLoop: Already running"); + ERROR_LOG(Log::G3D, "runRenderLoop: Already running"); return false; } // The previous thread gave up by itself (failed surface init). Reap it so we can try again. - WARN_LOG(Log::G3D, "runVulkanRenderLoop: Joining a render thread that had already exited"); + WARN_LOG(Log::G3D, "runRenderLoop: Joining a render thread that had already exited"); g_renderLoopThread.join(); g_renderLoopThread = std::thread(); } @@ -1668,12 +1681,12 @@ extern "C" jboolean JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_runVulkanRende // Set before the thread exists, so an exit request can't slip in ahead of it. renderLoopRunning = true; - g_renderLoopThread = std::thread(VulkanEmuThread, wnd, graphicsContext); + g_renderLoopThread = std::thread(RenderLoopThread, wnd, graphicsContext); return true; } -extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitVulkanRenderLoop(JNIEnv * env, jobject obj) { - // Like runVulkanRenderLoop, this is only called from the UI thread, so the thread object is ours. +extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitRenderLoop(JNIEnv * env, jobject obj) { + // Like runRenderLoop, this is only called from the UI thread, so the thread object is ours. // Don't go by renderLoopRunning - the thread might have bailed by itself, and still needs joining. if (!g_renderLoopThread.joinable()) { INFO_LOG(Log::System, "Render loop not running, nothing to join"); @@ -1687,21 +1700,26 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitVulkanR // TODO: Merge with the Win32 EmuThread and so on, and the Java EmuThread? // This function must release the wnd reference. -static void VulkanEmuThread(ANativeWindow *wnd, GraphicsContext *graphicsContext) { - SetCurrentThreadName("EmuThread"); +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"); AndroidJNIThreadContext ctx; JNIEnv *env = getEnv(); _assert_(graphicsContext); if (exitRenderLoop) { - WARN_LOG(Log::G3D, "runVulkanRenderLoop: ExitRenderLoop requested at start, skipping the whole thing."); + WARN_LOG(Log::G3D, "RenderLoopThread: ExitRenderLoop requested at start, skipping the whole thing."); renderLoopRunning = false; ANativeWindow_release(wnd); return; } - WARN_LOG(Log::G3D, "runVulkanRenderLoop. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", + WARN_LOG(Log::G3D, "RenderLoopThread. display_xres=%d display_yres=%d desiredBackbufferSizeX=%d desiredBackbufferSizeY=%d", display_xres, display_yres, desiredBackbufferSizeX, desiredBackbufferSizeY); std::string errorMessage; @@ -1717,11 +1735,23 @@ static void VulkanEmuThread(ANativeWindow *wnd, GraphicsContext *graphicsContext } renderer_inited = true; - RunMainLoop(graphicsContext, new NativeApplication(), [](GraphicsContext *graphicsContext) { + const auto frame = [](GraphicsContext *graphicsContext) { NativeFrame(graphicsContext); 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); + // This is the GL "GPU thread" - the loop Java's displayRender used to drive one call at a + // time. ThreadFrame returns false once the emu thread is done producing. + while (!exitRenderLoop && graphicsContext->ThreadFrame()) {} + // 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/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index c23dcc30f2..86bb1b6bcc 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -641,9 +641,11 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList sizeManager.checkDisplayMeasurements(); } - public native boolean runVulkanRenderLoop(Surface surface); + // Starts the native render loop thread. Used by Vulkan, and by OpenGL when the native side + // owns EGL rather than the GLSurfaceView in the javaGL path. + public native boolean runRenderLoop(Surface surface); // Tells the render loop thread to exit, so we can restart it. - public native void requestExitVulkanRenderLoop(); + public native void requestExitRenderLoop(); @Override public void onCreate(Bundle savedInstanceState) { @@ -942,7 +944,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList Log.w(TAG, "startRenderLoopThread: Starting thread"); applyFrameRate(mSurface, 60.0f); - runVulkanRenderLoop(mSurface); + runRenderLoop(mSurface); } private synchronized void joinRenderLoopThread() { @@ -952,8 +954,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList } // This will wait until the thread has exited. - Log.i(TAG, "requestExitVulkanRenderLoop"); - requestExitVulkanRenderLoop(); + Log.i(TAG, "requestExitRenderLoop"); + requestExitRenderLoop(); } void setupSystemUiCallback() {