mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-18 08:50:29 +02:00
Merge pull request #22451 from hrydgard/android-egl-replace-javagl
Android: Replace JavaGL init with EGL
This commit is contained in:
15 files changed
+454
-383
No files matched your search
@@ -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
|
||||
)
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+58
-21
@@ -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<bool(GraphicsContext *)> frame, std::function<bool()> 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<bool(GraphicsContext *)> 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;
|
||||
}
|
||||
@@ -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<bool(GraphicsContext *)> 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<bool(GraphicsContext *)> frame, std::function<bool()> shouldExit = nullptr);
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 <android/native_window.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <EGL/egl.h>
|
||||
|
||||
#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;
|
||||
};
|
||||
+31
-159
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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/#!/")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user