From 812d8f4c09a2791b37206cee014ffa42361abaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 05:27:49 +0200 Subject: [PATCH] Android GL: Delete the JavaGL path, leaving only native EGL The EGL path from the previous commit is now the only one, so GLSurfaceView and everything that existed to serve it are gone: - NativeRenderer.java and NativeGLSurfaceView.java, deleted. - The javaGL branches in PpssppActivity. GL and Vulkan now share one lifecycle: a plain SurfaceView, startRenderLoopThread() when the surface arrives, joinRenderLoopThread() when it goes away. - The "androidJavaGL" queryConfig, and the AndroidNativeEGL setting and its developer-tools checkbox. - displayInit/displayRender, and with them the "Second time, joining the emuthread and starting it up again" dance - restarting the emu thread from inside a JNI callback because a lost context was only observable as a second onSurfaceCreated. - GraphicsContext::NotifyContextLost, which existed solely because that join drained the render queue on a thread with no current context. The render thread now joins the emu thread with the context still current, so the drain is real work and nothing has to be skipped. - g_emuThread, now that nothing starts an emu thread outside the render loop. Three things displayInit/displayRender did that nothing else did, moved rather than dropped: - EnterVR and the per-frame StartVRRender/UpdateVRInput/FinishVRRender hooks now live in RenderLoopThread's GL branch. Java's GL thread could simply return early and wait for the next vsync when the VR session was inactive; this loop drives itself, so that case sleeps 16ms instead of spinning. Not tested - no headset here. - RECREATE_VIEWS after the emu thread starts, as before. Vulkan never did this, so it's a candidate for removal when the two paths converge. - The thin3d error callback, which surfaces GL errors as OSD messages. That one moved into AndroidEGLGraphicsContext::InitSurface, next to the rest of the context's own setup, matching WindowsGLContext. Tested on a nubia Z2464N (Unisoc UMS9632, Mali-G57, Android 15): cold start, UI, booting a homebrew, backgrounding and resuming both in the menu and in-game, and a density change, which destroys and recreates the activity the way a rotation does. Every cycle tears the EGL context down and builds a new one, and the renderer comes back with FBOs and textures intact. The config ladder's first choice, RGBA8888 D24S8, is what the Java chooser landed on here too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC --- Common/GPU/GraphicsContext.h | 4 - Common/GPU/OpenGL/OpenGLGraphicsContext.h | 6 - Common/GPU/Vulkan/VulkanQueueRunner.cpp | 2 +- Core/Config.cpp | 1 - Core/Config.h | 6 - UI/DeveloperToolsScreen.cpp | 11 -- android/jni/AndroidEGLGraphicsContext.cpp | 22 ++- android/jni/AndroidEGLGraphicsContext.h | 19 +- android/jni/app-android.cpp | 182 ++++-------------- android/src/org/ppsspp/ppsspp/NativeApp.java | 2 - .../ppsspp/ppsspp/NativeGLSurfaceView.java | 22 --- .../src/org/ppsspp/ppsspp/NativeRenderer.java | 52 ----- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 137 +++---------- 13 files changed, 88 insertions(+), 378 deletions(-) delete mode 100644 android/src/org/ppsspp/ppsspp/NativeGLSurfaceView.java delete mode 100644 android/src/org/ppsspp/ppsspp/NativeRenderer.java 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 2214830045..76895759e4 100644 --- a/Common/GPU/OpenGL/OpenGLGraphicsContext.h +++ b/Common/GPU/OpenGL/OpenGLGraphicsContext.h @@ -33,12 +33,6 @@ public: renderManager_->ThreadEnd(); } - void NotifyContextLost() override { - if (renderManager_) { - renderManager_->SetSkipGLCalls(); - } - } - // Call from emu thread void NotifyEmuThreadExit() override { renderManager_->SetSkipGLCalls(); 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/Config.cpp b/Core/Config.cpp index df0a205302..a82c9cc8c5 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -704,7 +704,6 @@ 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 b369dadc5e..ca21fb0658 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -569,12 +569,6 @@ 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 5a46cc1ba0..c570b671ae 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -539,17 +539,6 @@ 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/AndroidEGLGraphicsContext.cpp b/android/jni/AndroidEGLGraphicsContext.cpp index 6bc5d2d533..962a093609 100644 --- a/android/jni/AndroidEGLGraphicsContext.cpp +++ b/android/jni/AndroidEGLGraphicsContext.cpp @@ -19,6 +19,7 @@ #include "Common/Log.h" #include "Common/StringUtils.h" +#include "Common/System/OSD.h" #include "Common/VR/PPSSPPVR.h" #include "android/jni/AndroidEGLGraphicsContext.h" @@ -66,7 +67,7 @@ struct ConfigAttempt { // 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. +// 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 }, @@ -139,9 +140,9 @@ bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, vo 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. + // 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(); @@ -151,6 +152,7 @@ bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, vo // 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)) { @@ -192,6 +194,12 @@ bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, vo 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 @@ -201,9 +209,9 @@ bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, vo }); // 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. + // 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; } diff --git a/android/jni/AndroidEGLGraphicsContext.h b/android/jni/AndroidEGLGraphicsContext.h index 27997ec315..dde60aad60 100644 --- a/android/jni/AndroidEGLGraphicsContext.h +++ b/android/jni/AndroidEGLGraphicsContext.h @@ -23,18 +23,15 @@ 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). +// 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 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. +// 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 diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 458dfa5506..e034d6584a 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -88,7 +88,6 @@ 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" @@ -115,9 +114,6 @@ enum class EmuThreadState { STOPPED, }; -// OpenGL emu thread -static std::thread g_emuThread; - AndroidAudioState *g_audioState; struct FrameCommand { @@ -572,18 +568,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"; - } - // 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") { return g_Config.bAudioMixWithOthers ? "1" : "0"; } else { @@ -780,15 +764,9 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init retry: switch (g_Config.iGPUBackend) { case (int)GPUBackend::OPENGL: - 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"); - } + 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: { @@ -909,20 +887,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"); - // 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."); - 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"); @@ -945,79 +911,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); @@ -1157,38 +1050,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: @@ -1649,8 +1510,7 @@ std::thread g_renderLoopThread; static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContext); -// 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. +// 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) { @@ -1744,9 +1604,35 @@ static void RenderLoopThread(ANativeWindow *wnd, GraphicsContext *graphicsContex // 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 + + // The views were created against the previous surface, if there was one. + System_PostUIMessage(UIMessage::RECREATE_VIEWS); + + if (IsVREnabled()) { + // Only ever a GL thing, which is why it lives in this branch. + static bool vrFirstStart = true; + EnterVR(vrFirstStart); + vrFirstStart = false; + } + + // This is the GL "GPU thread" - the loop Java's GLSurfaceView used to drive one call at a // time. ThreadFrame returns false once the emu thread is done producing. - while (!exitRenderLoop && graphicsContext->ThreadFrame()) {} + while (!exitRenderLoop) { + if (IsVREnabled() && !StartVRRender()) { + // The session isn't active, so there's no frame to render into. Java's GL thread + // just missed a vsync here; we drive the loop ourselves, so pace it by hand + // rather than spinning. + sleep_ms(16, "vr-session-idle"); + continue; + } + if (!graphicsContext->ThreadFrame()) { + break; + } + if (IsVREnabled()) { + UpdateVRInput(g_Config.bHapticFeedback, g_display.dpi_scale_x, g_display.dpi_scale_y); + FinishVRRender(); + } + } // Also drains whatever the emu thread still has queued, so it can't block on us. EmuThread_Join(graphicsContext, emuThread); } else { 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 86bb1b6bcc..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,8 +631,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList sizeManager.checkDisplayMeasurements(); } - // 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. + // 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 requestExitRenderLoop(); @@ -713,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. @@ -903,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; @@ -948,11 +890,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList } 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, "requestExitRenderLoop"); requestExitRenderLoop(); @@ -973,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; @@ -1034,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); @@ -1081,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"); } @@ -1475,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/#!/")) {