Files
ppsspp/android/jni/AndroidEGLGraphicsContext.cpp
T
Henrik RydgårdandClaude Opus 5 812d8f4c09 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:22:25 +02:00

248 lines
9.6 KiB
C++

// 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;
}