Files
ppsspp/android/jni/AndroidEGLGraphicsContext.cpp
T
Henrik RydgårdandClaude Opus 5 f868976af2 Android GL: Add a native EGL path, behind a dev toggle
First step towards removing JavaGL. The point isn't EGL itself - it's who
owns the render thread. With GLSurfaceView, Java creates the thread and the
context and calls us back, so a lost context is only observable after the
fact as a second onSurfaceCreated. That's what forces displayInit to tear
down and restart the emu thread from inside a JNI callback, and to call
NotifyContextLost first because the queue is about to be drained on a thread
with no context. None of that is needed when we own EGL: the render thread
joins the emu thread itself, with the context still current, so the drain is
real work rather than something to skip.

AndroidEGLGraphicsContext subclasses OpenGLGraphicsContext (whose own
InitSurface assumes someone else already made a context current) and adds
display/config/surface/context plus a swap function. With it, GL reuses the
Vulkan lifecycle wholesale: queryConfig("androidJavaGL") returns false, so
Java builds a plain SurfaceView and starts the native render loop when a
surface arrives - including all of the surface/pause ordering fixes from the
previous branch. runVulkanRenderLoop/requestExitVulkanRenderLoop are now
runRenderLoop/requestExitRenderLoop and serve both backends; the thread they
spawn branches on NeedsSeparateEmuThread, because the two backends split the
work in opposite directions (Vulkan: this thread is the main loop and the
render manager spawns the render thread; GL: this thread *is* the render
thread and spawns the emu thread).

Deliberately a port, not an improvement:

- GLES client version still follows NativeRenderer - 3 for VR, 2 otherwise.
  Not a cap; GLES3 is detected from GL_VERSION at runtime.
- No swap-interval function. GLSurfaceView gave us no way to set one, so
  PresentMode::IMMEDIATE has always been ignored here, and honoring it now
  would be a behavior change. Worth revisiting once this path is the only one.

The config ladder does take the user's word that Mali 400 and the Xperia Play
are no longer a factor: it asks for RGBA8888/D24S8 and falls back twice,
rather than reproducing GLSurfaceView's ask-for-nothing default that the
comments in PpssppActivity.onCreate exist to explain. ANativeWindow's format
is set from EGL_NATIVE_VISUAL_ID, which is the proper fix for the EGL_BAD_MATCH
those comments describe.

Off by default, behind Developer tools / Graphics / "Use native EGL instead
of GLSurfaceView" (OpenGL backend, Android only). Needs an app restart, since
the context is chosen in NativeApp.init.

Builds for arm64-v8a and on Windows; NOT yet run on a device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:22:25 +02:00

240 lines
9.0 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/VR/PPSSPPVR.h"
#include "android/jni/AndroidEGLGraphicsContext.h"
// EGL_OPENGL_ES3_BIT_KHR, from EGL_KHR_create_context. Not in every platform header.
#ifndef EGL_OPENGL_ES3_BIT_KHR
#define EGL_OPENGL_ES3_BIT_KHR 0x0040
#endif
namespace {
const char *EGLErrorToString(EGLint error) {
switch (error) {
case EGL_SUCCESS: return "EGL_SUCCESS";
case EGL_NOT_INITIALIZED: return "EGL_NOT_INITIALIZED";
case EGL_BAD_ACCESS: return "EGL_BAD_ACCESS";
case EGL_BAD_ALLOC: return "EGL_BAD_ALLOC";
case EGL_BAD_ATTRIBUTE: return "EGL_BAD_ATTRIBUTE";
case EGL_BAD_CONTEXT: return "EGL_BAD_CONTEXT";
case EGL_BAD_CONFIG: return "EGL_BAD_CONFIG";
case EGL_BAD_CURRENT_SURFACE: return "EGL_BAD_CURRENT_SURFACE";
case EGL_BAD_DISPLAY: return "EGL_BAD_DISPLAY";
case EGL_BAD_SURFACE: return "EGL_BAD_SURFACE";
case EGL_BAD_MATCH: return "EGL_BAD_MATCH";
case EGL_BAD_PARAMETER: return "EGL_BAD_PARAMETER";
case EGL_BAD_NATIVE_PIXMAP: return "EGL_BAD_NATIVE_PIXMAP";
case EGL_BAD_NATIVE_WINDOW: return "EGL_BAD_NATIVE_WINDOW";
case EGL_CONTEXT_LOST: return "EGL_CONTEXT_LOST";
default: return "(unknown EGL error)";
}
}
std::string EGLFailure(const char *what) {
const EGLint error = eglGetError();
return StringFromFormat("%s failed: %s (%04x)", what, EGLErrorToString(error), error);
}
struct ConfigAttempt {
const char *desc;
EGLint red, green, blue, alpha, depth, stencil;
};
// Most to least desirable. The PSP only needs 16 bits of depth, but ask for 24 first - it is what
// essentially every driver offers anyway, and asking for less does not make a 16-bit buffer appear.
// Stencil is not optional in practice: without it, non-buffered rendering has nothing to work with.
//
// The last entry is the fallback: ask for the bare minimum and take whatever eglChooseConfig puts
// first, which is roughly what GLSurfaceView's default chooser does today.
const ConfigAttempt kConfigAttempts[] = {
{ "RGBA8888 D24S8", 8, 8, 8, 8, 24, 8 },
{ "RGBA8888 D16S8", 8, 8, 8, 8, 16, 8 },
{ "RGB565 D16S8", 5, 6, 5, 0, 16, 8 },
{ "anything", 5, 6, 5, 0, 16, 0 },
};
} // namespace
bool AndroidEGLGraphicsContext::ChooseConfig(bool wantGLES3, std::string *errorMessage) {
const EGLint renderableType = wantGLES3 ? EGL_OPENGL_ES3_BIT_KHR : EGL_OPENGL_ES2_BIT;
for (const ConfigAttempt &attempt : kConfigAttempts) {
const EGLint attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE, renderableType,
EGL_RED_SIZE, attempt.red,
EGL_GREEN_SIZE, attempt.green,
EGL_BLUE_SIZE, attempt.blue,
EGL_ALPHA_SIZE, attempt.alpha,
EGL_DEPTH_SIZE, attempt.depth,
EGL_STENCIL_SIZE, attempt.stencil,
EGL_NONE
};
EGLConfig config = nullptr;
EGLint numConfigs = 0;
if (eglChooseConfig(display_, attribs, &config, 1, &numConfigs) && numConfigs > 0) {
config_ = config;
EGLint red = 0, green = 0, blue = 0, alpha = 0, depth = 0, stencil = 0, samples = 0;
eglGetConfigAttrib(display_, config_, EGL_RED_SIZE, &red);
eglGetConfigAttrib(display_, config_, EGL_GREEN_SIZE, &green);
eglGetConfigAttrib(display_, config_, EGL_BLUE_SIZE, &blue);
eglGetConfigAttrib(display_, config_, EGL_ALPHA_SIZE, &alpha);
eglGetConfigAttrib(display_, config_, EGL_DEPTH_SIZE, &depth);
eglGetConfigAttrib(display_, config_, EGL_STENCIL_SIZE, &stencil);
eglGetConfigAttrib(display_, config_, EGL_SAMPLES, &samples);
INFO_LOG(Log::G3D, "EGL config (asked for %s): R%dG%dB%dA%d depth=%d stencil=%d samples=%d",
attempt.desc, red, green, blue, alpha, depth, stencil, samples);
if (stencil == 0) {
WARN_LOG(Log::G3D, "EGL config has no stencil buffer - non-buffered rendering will misbehave.");
}
return true;
}
INFO_LOG(Log::G3D, "No EGL config for %s, trying the next one", attempt.desc);
}
*errorMessage = "Found no usable EGL config";
return false;
}
bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) {
_dbg_assert_(winsys == WINDOWSYSTEM_ANDROID);
wnd_ = (ANativeWindow *)data1;
if (!wnd_) {
*errorMessage = "AndroidEGLGraphicsContext::InitSurface: no native window";
return false;
}
display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display_ == EGL_NO_DISPLAY) {
*errorMessage = EGLFailure("eglGetDisplay");
return false;
}
EGLint eglMajor = 0, eglMinor = 0;
if (!eglInitialize(display_, &eglMajor, &eglMinor)) {
*errorMessage = EGLFailure("eglInitialize");
display_ = EGL_NO_DISPLAY;
return false;
}
INFO_LOG(Log::G3D, "EGL %d.%d initialized (vendor: %s)", eglMajor, eglMinor,
eglQueryString(display_, EGL_VENDOR));
// Mirrors NativeRenderer's setEGLContextClientVersion(isVRDevice() ? 3 : 2). Asking for 2 is
// not a cap: GLES3 is detected from GL_VERSION at runtime (see GLFeatures.cpp), which is how
// the Java path has always ended up using GLES3 on devices that have it.
const bool wantGLES3 = IsVREnabled();
if (!ChooseConfig(wantGLES3, errorMessage)) {
DestroyEGL();
return false;
}
// Line the window's pixel format up with the config we picked. Skipping this is how you get
// EGL_BAD_MATCH out of eglCreateWindowSurface on drivers that take the window format
// literally - historically the reason the Java path asked for no particular config at all.
// Width and height stay 0 so whatever size Java set (see iAndroidHwScale) is left alone.
EGLint nativeVisualID = 0;
if (eglGetConfigAttrib(display_, config_, EGL_NATIVE_VISUAL_ID, &nativeVisualID)) {
ANativeWindow_setBuffersGeometry(wnd_, 0, 0, nativeVisualID);
} else {
WARN_LOG(Log::G3D, "Couldn't get EGL_NATIVE_VISUAL_ID, leaving the window format alone");
}
const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, wantGLES3 ? 3 : 2, EGL_NONE };
context_ = eglCreateContext(display_, config_, EGL_NO_CONTEXT, contextAttribs);
if (context_ == EGL_NO_CONTEXT) {
*errorMessage = EGLFailure("eglCreateContext");
DestroyEGL();
return false;
}
surface_ = eglCreateWindowSurface(display_, config_, wnd_, nullptr);
if (surface_ == EGL_NO_SURFACE) {
*errorMessage = EGLFailure("eglCreateWindowSurface");
DestroyEGL();
return false;
}
if (!eglMakeCurrent(display_, surface_, surface_, context_)) {
*errorMessage = EGLFailure("eglMakeCurrent");
DestroyEGL();
return false;
}
EGLint surfaceWidth = 0, surfaceHeight = 0;
eglQuerySurface(display_, surface_, EGL_WIDTH, &surfaceWidth);
eglQuerySurface(display_, surface_, EGL_HEIGHT, &surfaceHeight);
INFO_LOG(Log::G3D, "EGL context current, surface is %dx%d", surfaceWidth, surfaceHeight);
// GL is current on this thread now, so the shared setup (extension check, thin3d context,
// render manager) can run.
if (!OpenGLGraphicsContext::InitSurface(winsys, data1, data2, errorMessage)) {
DestroyEGL();
return false;
}
renderManager_->SetSwapFunction([this]() {
if (!eglSwapBuffers(display_, surface_)) {
// Most likely the surface went away under us. The render loop is about to be asked to
// exit anyway, so just complain rather than trying to recover from here.
WARN_LOG(Log::G3D, "%s", EGLFailure("eglSwapBuffers").c_str());
}
});
// NOTE: Deliberately *not* calling SetSwapIntervalFunction. GLSurfaceView gave us no way to
// change the interval, so PresentMode::IMMEDIATE has always been a no-op here, and honoring it
// now would be a behavior change rather than a port. Worth revisiting once this path is the
// only one.
return true;
}
void AndroidEGLGraphicsContext::ShutdownSurface() {
// Order matters: the render manager's teardown still issues GL calls, so the context has to
// stay current until it's done.
OpenGLGraphicsContext::ShutdownSurface();
DestroyEGL();
}
void AndroidEGLGraphicsContext::DestroyEGL() {
if (display_ == EGL_NO_DISPLAY) {
return;
}
INFO_LOG(Log::G3D, "AndroidEGLGraphicsContext::DestroyEGL");
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (surface_ != EGL_NO_SURFACE) {
eglDestroySurface(display_, surface_);
surface_ = EGL_NO_SURFACE;
}
if (context_ != EGL_NO_CONTEXT) {
eglDestroyContext(display_, context_);
context_ = EGL_NO_CONTEXT;
}
// eglTerminate only tears down this thread's binding of the display, which is refcounted per
// process - so this is safe even though the display is shared.
eglTerminate(display_);
eglReleaseThread();
display_ = EGL_NO_DISPLAY;
config_ = nullptr;
wnd_ = nullptr;
}