From f868976af2938377c1e8a3a89295727ac34d7372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 00:55:55 +0200 Subject: [PATCH] Android GL: Add a native EGL path, behind a dev toggle First step towards removing JavaGL. The point isn't EGL itself - it's who owns the render thread. With GLSurfaceView, Java creates the thread and the context and calls us back, so a lost context is only observable after the fact as a second onSurfaceCreated. That's what forces displayInit to tear down and restart the emu thread from inside a JNI callback, and to call NotifyContextLost first because the queue is about to be drained on a thread with no context. None of that is needed when we own EGL: the render thread joins the emu thread itself, with the context still current, so the drain is real work rather than something to skip. AndroidEGLGraphicsContext subclasses OpenGLGraphicsContext (whose own InitSurface assumes someone else already made a context current) and adds display/config/surface/context plus a swap function. With it, GL reuses the Vulkan lifecycle wholesale: queryConfig("androidJavaGL") returns false, so Java builds a plain SurfaceView and starts the native render loop when a surface arrives - including all of the surface/pause ordering fixes from the previous branch. runVulkanRenderLoop/requestExitVulkanRenderLoop are now runRenderLoop/requestExitRenderLoop and serve both backends; the thread they spawn branches on NeedsSeparateEmuThread, because the two backends split the work in opposite directions (Vulkan: this thread is the main loop and the render manager spawns the render thread; GL: this thread *is* the render thread and spawns the emu thread). Deliberately a port, not an improvement: - GLES client version still follows NativeRenderer - 3 for VR, 2 otherwise. Not a cap; GLES3 is detected from GL_VERSION at runtime. - No swap-interval function. GLSurfaceView gave us no way to set one, so PresentMode::IMMEDIATE has always been ignored here, and honoring it now would be a behavior change. Worth revisiting once this path is the only one. The config ladder does take the user's word that Mali 400 and the Xperia Play are no longer a factor: it asks for RGBA8888/D24S8 and falls back twice, rather than reproducing GLSurfaceView's ask-for-nothing default that the comments in PpssppActivity.onCreate exist to explain. ANativeWindow's format is set from EGL_NATIVE_VISUAL_ID, which is the proper fix for the EGL_BAD_MATCH those comments describe. Off by default, behind Developer tools / Graphics / "Use native EGL instead of GLSurfaceView" (OpenGL backend, Android only). Needs an app restart, since the context is chosen in NativeApp.init. Builds for arm64-v8a and on Windows; NOT yet run on a device. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC --- CMakeLists.txt | 2 + Common/GPU/OpenGL/OpenGLGraphicsContext.h | 4 +- Core/Config.cpp | 1 + Core/Config.h | 6 + UI/DeveloperToolsScreen.cpp | 11 + android/jni/Android.mk | 1 + android/jni/AndroidEGLGraphicsContext.cpp | 239 ++++++++++++++++++ android/jni/AndroidEGLGraphicsContext.h | 61 +++++ android/jni/app-android.cpp | 76 ++++-- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 12 +- 10 files changed, 384 insertions(+), 29 deletions(-) create mode 100644 android/jni/AndroidEGLGraphicsContext.cpp create mode 100644 android/jni/AndroidEGLGraphicsContext.h 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() {