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/GraphicsContext.h b/Common/GPU/GraphicsContext.h index ca05a20a17..a4b2667a41 100644 --- a/Common/GPU/GraphicsContext.h +++ b/Common/GPU/GraphicsContext.h @@ -54,10 +54,6 @@ 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() {} diff --git a/Common/GPU/OpenGL/OpenGLGraphicsContext.h b/Common/GPU/OpenGL/OpenGLGraphicsContext.h index 082ac4a819..76895759e4 100644 --- a/Common/GPU/OpenGL/OpenGLGraphicsContext.h +++ b/Common/GPU/OpenGL/OpenGLGraphicsContext.h @@ -33,19 +33,15 @@ public: renderManager_->ThreadEnd(); } - void NotifyContextLost() override { - if (renderManager_) { - renderManager_->SetSkipGLCalls(); - } - } - // Call from emu thread void NotifyEmuThreadExit() override { renderManager_->SetSkipGLCalls(); 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/Common/GPU/Vulkan/VulkanQueueRunner.cpp b/Common/GPU/Vulkan/VulkanQueueRunner.cpp index 2e99af395f..7ea72f7bd4 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.cpp +++ b/Common/GPU/Vulkan/VulkanQueueRunner.cpp @@ -8,7 +8,7 @@ using namespace PPSSPP_VK; -// Debug help: adb logcat -s DEBUG AndroidRuntime PPSSPPNativeActivity PPSSPP NativeGLView NativeRenderer NativeSurfaceView PowerSaveModeReceiver InputDeviceState PpssppActivity CameraHelper PPSSPPSizeManager +// Debug help: adb logcat -s DEBUG AndroidRuntime PPSSPPNativeActivity PPSSPP NativeSurfaceView PowerSaveModeReceiver InputDeviceState PpssppActivity CameraHelper PPSSPPSizeManager static void MergeRenderAreaRectInto(VkRect2D *dest, const VkRect2D &src) { if (dest->offset.x > src.offset.x) { 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/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..962a093609 --- /dev/null +++ b/android/jni/AndroidEGLGraphicsContext.cpp @@ -0,0 +1,247 @@ +// 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/System/OSD.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 used to do. +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 what NativeRenderer used to ask for: 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 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. + // That is also why we can afford to be picky in kConfigAttempts where Java could not. + // 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; + } + + // As on Windows (see WindowsGLContext), surface GL errors to the user. The Java path did this + // from displayInit; now that the context owns its own init, it belongs here. + draw_->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { + g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); + }, nullptr); + + 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 was always a no-op on Android GL, and this + // class set out to behave identically. Now that it's the only path, eglSwapInterval(0) is + // there for the taking - but that's a behavior change, so it wants its own commit. + + 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..dde60aad60 --- /dev/null +++ b/android/jni/AndroidEGLGraphicsContext.h @@ -0,0 +1,58 @@ +// 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. Until 2026 this was Java's job, through a +// GLSurfaceView (NativeRenderer.java / NativeGLSurfaceView.java, the "JavaGL" path). +// +// The point is not EGL itself but who owns the render thread. With GLSurfaceView, Java created +// the thread and the context and called us back, so a lost context was only ever observable +// after the fact, as a second onSurfaceCreated - which forced app-android.cpp 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. +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..492252290a 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -88,7 +88,7 @@ struct JNIEnv {}; #include "Common/GPU/Vulkan/VulkanLoader.h" #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" @@ -106,17 +106,6 @@ struct JNIEnv {}; #include "app-android.h" -enum class EmuThreadState { - DISABLED, - START_REQUESTED, - RUNNING, - QUIT_REQUESTED, - STOPPED, -}; - -// OpenGL emu thread -static std::thread g_emuThread; - AndroidAudioState *g_audioState; struct FrameCommand { @@ -571,13 +560,6 @@ static std::string QueryConfig(std::string_view query) { return g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation()).bImmersiveMode ? "1" : "0"; } else if (query == "sustainedPerformanceMode") { return g_Config.bSustainedPerformanceMode ? "1" : "0"; - } else if (query == "androidJavaGL") { - // If we're using Vulkan, we say no... need C++ to use Vulkan. - if (GetGPUBackend() == GPUBackend::VULKAN) { - return "false"; - } - // Otherwise, some devices prefer the Java init so play it safe. - return "true"; } else if (query == "audioMixWithOthers") { return g_Config.bAudioMixWithOthers ? "1" : "0"; } else { @@ -774,9 +756,9 @@ 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"); + INFO_LOG(Log::System, "NativeApp.init() -- creating OpenGL context"); + graphicsContext = new AndroidEGLGraphicsContext(); + // Like Vulkan, we wait for a surface before doing anything - see runRenderLoop. break; case (int)GPUBackend::VULKAN: { @@ -897,18 +879,8 @@ 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. - // 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); - - INFO_LOG(Log::System, "EmuThread joined."); - graphicsContext->ShutdownSurface(); - INFO_LOG(Log::System, "Graphics context now shut down from NativeApp_shutdown"); - } - + // Nothing to join here: the render loop thread owns both itself and the emu thread, and + // requestExitRenderLoop has already brought them down along with the surface. { if (graphicsContext) { INFO_LOG(Log::G3D, "Shutting down renderer"); @@ -931,79 +903,6 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_shutdown(JNIEnv *, jclass) { INFO_LOG(Log::System, "NativeApp.shutdown() -- end"); } -// JavaEGL. This doesn't get called on the Vulkan path. -// This gets called from onSurfaceCreated. -extern "C" jboolean Java_org_ppsspp_ppsspp_NativeRenderer_displayInit(JNIEnv * env, jobject obj) { - if (!graphicsContext) { - ERROR_LOG(Log::G3D, "NativeApp.displayInit() - graphicsContext is null!"); - return false; - } - - _assert_(graphicsContext->NeedsSeparateEmuThread()); - - INFO_LOG(Log::G3D, "NativeApp.displayInit()"); - bool firstStart = !renderer_inited; - - // We should be running on the render thread here. - std::string errorMessage; - if (!renderer_inited) { - INFO_LOG(Log::G3D, "NativeApp.displayInit() first time"); - if (!graphicsContext->InitSurface(WINDOWSYSTEM_ANDROID, nullptr, nullptr, &errorMessage)) { - System_Toast("Graphics initialization failed. Quitting."); - return false; - } - - graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { - g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); - }, nullptr); - - // This is where we start the emuthread now - after InitFromRenderThread. This eliminates a race condition. - g_emuThread = EmuThread_Start(graphicsContext, new NativeApplication(), [](GraphicsContext *graphicsContext) { - NativeFrame(graphicsContext); - ProcessFrameCommands(); - return true; - }); - renderer_inited = true; - } else { - // Would be really nice if we could get something on the GL thread immediately when shutting down, - // 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(); - - INFO_LOG(Log::G3D, "Shut down both threads. Now let's bring it up again!"); - - if (!graphicsContext->InitSurface(WINDOWSYSTEM_ANDROID, nullptr, nullptr, &errorMessage)) { - System_Toast(("Graphics initialization failed: Quitting: " + errorMessage).c_str()); - return false; - } - - graphicsContext->GetDrawContext()->SetErrorCallback([](const char *shortDesc, const char *details, void *userdata) { - g_OSD.Show(OSDType::MESSAGE_ERROR, details, 5.0); - }, nullptr); - - g_emuThread = EmuThread_Start(graphicsContext, new NativeApplication(), [](GraphicsContext *graphicsContext) { - NativeFrame(graphicsContext); - ProcessFrameCommands(); - return true; - }); - - INFO_LOG(Log::G3D, "Restored."); - } - - System_PostUIMessage(UIMessage::RECREATE_VIEWS); - - if (IsVREnabled()) { - EnterVR(firstStart); - } - return true; -} - extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_backbufferResize(JNIEnv *, jclass, jint pixel_xres, jint pixel_yres, jint format) { INFO_LOG(Log::System, "NativeApp.backbufferResize(%d x %d)", pixel_xres, pixel_yres); @@ -1143,38 +1042,6 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEn } } -// This doesn't get called on the Vulkan path. -// We don't need a render thread "loop" as this gets called repeatedly by the system, by a system -// render thread. -extern "C" void Java_org_ppsspp_ppsspp_NativeRenderer_displayRender(JNIEnv *env, jobject obj) { - static bool hasSetThreadName = false; - if (!hasSetThreadName) { - hasSetThreadName = true; - SetCurrentThreadName("AndroidRender"); - } - - if (IsVREnabled() && !StartVRRender()) { - return; - } - - // This is the "GPU thread". Call ThreadFrame. - if (!graphicsContext) { - return; - } - _assert_(graphicsContext->NeedsSeparateEmuThread()); - - if (!graphicsContext->ThreadFrame()) { - INFO_LOG(Log::G3D, "ThreadFrame returned false"); - // TODO: We should stop calling ThreadFrame here. - return; - } - - if (IsVREnabled()) { - UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y); - FinishVRRender(); - } -} - void System_AskForPermission(SystemPermission permission) { switch (permission) { case SYSTEM_PERMISSION_STORAGE: @@ -1633,25 +1500,23 @@ 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 both the Vulkan and the OpenGL path. +// 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 +1533,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"); @@ -1685,23 +1550,23 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_PpssppActivity_requestExitVulkanR exitRenderLoop = false; } -// 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) { + // 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) { - 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 +1582,18 @@ 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; - }); + }; + // 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(); }); + 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/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index a2b575e193..199d80af55 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -1,7 +1,5 @@ package org.ppsspp.ppsspp; -// Note that the display* methods are in NativeRenderer.java - import android.os.Build; import android.util.Log; import android.view.InputDevice; diff --git a/android/src/org/ppsspp/ppsspp/NativeGLSurfaceView.java b/android/src/org/ppsspp/ppsspp/NativeGLSurfaceView.java deleted file mode 100644 index 503adfc381..0000000000 --- a/android/src/org/ppsspp/ppsspp/NativeGLSurfaceView.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.ppsspp.ppsspp; - -// Touch-enabled GLSurfaceView. -// Used when javaGL = true. - -import android.annotation.SuppressLint; -import android.content.Context; -import android.opengl.GLSurfaceView; -import android.view.MotionEvent; - -public class NativeGLSurfaceView extends GLSurfaceView { - public NativeGLSurfaceView(Context context) { - super(context); - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouchEvent(final MotionEvent ev) { - NativeApp.processTouchEvent(ev); - return true; - } -} diff --git a/android/src/org/ppsspp/ppsspp/NativeRenderer.java b/android/src/org/ppsspp/ppsspp/NativeRenderer.java deleted file mode 100644 index c7837d807e..0000000000 --- a/android/src/org/ppsspp/ppsspp/NativeRenderer.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.ppsspp.ppsspp; - -import android.opengl.GLSurfaceView; -import android.util.Log; - -import javax.microedition.khronos.egl.EGL10; -import javax.microedition.khronos.egl.EGLConfig; -import javax.microedition.khronos.egl.EGLContext; -import javax.microedition.khronos.egl.EGLDisplay; -import javax.microedition.khronos.opengles.GL10; - -// Only used for the OpenGL backend. -public class NativeRenderer implements GLSurfaceView.Renderer { - private static final String TAG = "NativeRenderer"; - - public void onDrawFrame(GL10 unused /*use GLES20*/) { - displayRender(); - } - public void onSurfaceCreated(GL10 gl, EGLConfig config) { - Log.i(TAG, "NativeRenderer (OpenGL): onSurfaceCreated"); - - EGL10 egl = (EGL10)EGLContext.getEGL(); - if (egl != null) { - EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); - if (dpy != null) { - int[] val = new int[1]; - egl.eglGetConfigAttrib(dpy, config, EGL10.EGL_DEPTH_SIZE, val); - int depthBits = val[0]; - egl.eglGetConfigAttrib(dpy, config, EGL10.EGL_STENCIL_SIZE, val); - int stencilBits = val[0]; - Log.i(TAG, "EGL reports " + depthBits + " bits of depth and " + stencilBits + " bits of stencil."); - } else { - Log.e(TAG, "dpy == null"); - } - } else { - Log.e(TAG, "egl == null"); - } - - if (!displayInit()) { - Log.e(TAG, "Display init failed"); - } - } - - public void onSurfaceChanged(GL10 unused, int width, int height) { - Log.i(TAG, "NativeRenderer (OpenGL): onSurfaceChanged: width=" + width + " height=" + height); - } - - // Note: This also means "device lost" and you should reload - // all buffered objects. - public native boolean displayInit(); - public native void displayRender(); -} diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index c23dcc30f2..53d069369a 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -78,20 +78,13 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList // Allows us to skip a lot of initialization on secondary calls to onCreate. private static boolean initialized = false; - // False to use Vulkan, queried from C++ after NativeApp.init. - private static boolean javaGL = true; - // Lifecycle tracker, to detect erroneous states. private final LifeCycle lifeCycle = new LifeCycle(); - // Graphics and audio interfaces for Vulkan (javaGL = false) + // The surface we hand to the native render loop thread, for both OpenGL and Vulkan. private NativeSurfaceView mSurfaceView; private Surface mSurface; - // Graphics and audio interfaces for Java EGL (javaGL = true) - private NativeGLSurfaceView mGLSurfaceView; - protected NativeRenderer nativeRenderer; - // For accelerometer sensing. private SensorManager mSensorManager; private Sensor mAccelerometer; @@ -468,9 +461,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList NativeApp.audioConfig(optimalFramesPerBuffer, optimalSampleRate); NativeApp.init(model, deviceType, languageRegion, apkFilePath, dataDir, extStorageDir, externalFilesDir, nativeLibDir, additionalStorageDirs, cacheDir, shortcut, installerName, Build.VERSION.SDK_INT, Build.BOARD, smallestScreenWidthDp); - // Allow C++ to tell us to use JavaGL or not. - javaGL = "true".equalsIgnoreCase(NativeApp.queryConfig("androidJavaGL")); - sendInitialGrants(); // OK, config should be initialized, we can query for screen rotation. @@ -641,9 +631,10 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList sizeManager.checkDisplayMeasurements(); } - public native boolean runVulkanRenderLoop(Surface surface); + // Starts the native render loop thread, which owns the graphics context and its surface. + 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) { @@ -711,49 +702,14 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList updateAudioFocus(this.audioManager, this.audioFocusChangeListener); NativeApp.audioInit(); - if (javaGL) { - mGLSurfaceView = new NativeGLSurfaceView(this); - nativeRenderer = new NativeRenderer(); - mGLSurfaceView.setEGLContextClientVersion(isVRDevice() ? 3 : 2); + updateSystemUiVisibility(); - sizeManager.setSurfaceView(mGLSurfaceView); - setInsetsListener(mGLSurfaceView); + mSurfaceView = new NativeSurfaceView(this); + sizeManager.setSurfaceView(mSurfaceView); + setInsetsListener(mSurfaceView); + setContentView(mSurfaceView); - // Setup the GLSurface and ask android for the correct - // Number of bits for r, g, b, a, depth and stencil components - // The PSP only has 16-bit Z so that should be enough. - // Might want to change this for other apps (24-bit might be useful). - // Actually, we might be able to do without both stencil and depth in - // the back buffer, but that would kill non-buffered rendering. - - // It appears some gingerbread devices blow up if you use a config chooser at all ???? (Xperia Play) - //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - - // On some (especially older devices), things blow up later (EGL_BAD_MATCH) if we don't set - // the format here, if we specify that we want destination alpha in the config chooser, which we do. - // http://grokbase.com/t/gg/android-developers/11bj40jm4w/fall-back - - // Tried to mess around with config choosers (NativeEGLConfigChooser) here but fail completely on Xperia Play. - - // Then I tried to require 8888/16/8 but that backfired too, does not work on Mali 450 which is - // used in popular TVs and boxes like Mi Box. So we'll just get what we get, I guess... - - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && !Build.MANUFACTURER.equals("Amazon")) { - // mGLSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 8); - // } - - mGLSurfaceView.setRenderer(nativeRenderer); - setContentView(mGLSurfaceView); - } else { - updateSystemUiVisibility(); - - mSurfaceView = new NativeSurfaceView(this); - sizeManager.setSurfaceView(mSurfaceView); - setInsetsListener(mSurfaceView); - setContentView(mSurfaceView); - - // render loop thread will be started once we get a surface. - } + // The render loop thread will be started once we get a surface. if (!firstRun && shortcutParam != null && !shortcutParam.isEmpty()) { // The native side is already up, so it didn't see this through NativeApp.init. @@ -901,39 +857,27 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList public void notifySurface(Surface surface) { mSurface = surface; - if (!javaGL) { - if (!initialized) { - Log.e(TAG, "notifySurface: Saving surface, but can't start/stop threads while not initialized"); - return; - } + if (!initialized) { + Log.e(TAG, "notifySurface: Saving surface, but can't start/stop threads while not initialized"); + return; + } - // If we got a surface, this starts the thread. - if (mSurface != null) { - // applyFramerate is called in here. - Log.i(TAG, "notifySurface: got surface, starting thread."); - startRenderLoopThread(); - } else { - // The surface must not be touched once surfaceDestroyed returns. Normally onPause has - // already joined the thread and this does nothing, but that order isn't guaranteed. - Log.i(TAG, "notifySurface: Surface is gone, making sure the render thread is too."); - joinRenderLoopThread(); - } - } else if (mSurface != null) { - // JavaGL path. - // TODO: This might not be the best place to do this. Seems to cause a surface recreation - // unnecessarily. - Log.i(TAG, "notifySurface: Applying framerate."); - applyFrameRate(mSurface, 60.0f); + // If we got a surface, this starts the thread. + if (mSurface != null) { + // applyFramerate is called in here. + Log.i(TAG, "notifySurface: got surface, starting thread."); + startRenderLoopThread(); + } else { + // The surface must not be touched once surfaceDestroyed returns. Normally onPause has + // already joined the thread and this does nothing, but that order isn't guaranteed. + Log.i(TAG, "notifySurface: Surface is gone, making sure the render thread is too."); + joinRenderLoopThread(); } updateSustainedPerformanceMode(); } // The render loop thread (EmuThread) is now spawned from the native side. protected synchronized void startRenderLoopThread() { - if (javaGL) { - Log.e(TAG, "JavaGL mode - should not get into startRenderLoopThread."); - return; - } if (mSurface == null) { Log.w(TAG, "startRenderLoopThread - not starting thread, needs surface"); return; @@ -942,18 +886,13 @@ 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() { - if (javaGL) { - Log.e(TAG, "JavaGL - should not get into joinRenderLoopThread."); - return; - } - // This will wait until the thread has exited. - Log.i(TAG, "requestExitVulkanRenderLoop"); - requestExitVulkanRenderLoop(); + Log.i(TAG, "requestExitRenderLoop"); + requestExitRenderLoop(); } void setupSystemUiCallback() { @@ -971,13 +910,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList super.onDestroy(); lifeCycle.onDestroy(); - if (javaGL) { - nativeRenderer = null; - mGLSurfaceView = null; - } else { - mSurfaceView = null; - mSurface = null; - } + mSurfaceView = null; + mSurface = null; mSensorManager = null; mAccelerometer = null; @@ -1032,14 +966,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList InputManager inputManager = (InputManager)getSystemService(Context.INPUT_SERVICE); inputManager.unregisterInputDeviceListener(inputDeviceListener); - if (!javaGL) { - Log.i(TAG, "Joining render thread..."); - joinRenderLoopThread(); - Log.i(TAG, "Joined render thread"); - } else if (mGLSurfaceView != null) { - Log.i(TAG, "mGLSurfaceView.onPause..."); - mGLSurfaceView.onPause(); - } + Log.i(TAG, "Joining render thread..."); + joinRenderLoopThread(); + Log.i(TAG, "Joined render thread"); Log.i(TAG, "mSensorManager.unregisterListener..."); mSensorManager.unregisterListener(this); @@ -1079,12 +1008,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList (InputManager)getSystemService(Context.INPUT_SERVICE); inputManager.registerInputDeviceListener(inputDeviceListener, null); - if (!javaGL) { - // Restart the render loop. - startRenderLoopThread(); - } else if (mGLSurfaceView != null) { - mGLSurfaceView.onResume(); - } + // Restart the render loop. + startRenderLoopThread(); Log.i(TAG, "onResume end"); } @@ -1473,7 +1398,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList } public boolean processCommand(String command, String params) { - SurfaceView surfView = javaGL ? mGLSurfaceView : mSurfaceView; + SurfaceView surfView = mSurfaceView; if (command.equals("launchBrowser")) { // Special case for twitter if (params.startsWith("https://twitter.com/#!/")) { 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