Stage 5 of the JavaGL removal. Now that the GL path owns its own EGL context and
render thread, Android's two lifecycle branches and the Win32 one in
MainThreadFunc were the same code three times over, so this moves the branch into
one place - RunGraphicsLoop() in Core/EmuThread.cpp. It picks the thread
arrangement the context asks for: either this thread becomes the render thread
and spawns the emu thread (OpenGL), or it just runs the main loop because the
backend spawns its own render thread (Vulkan, D3D11). Android, Windows and
headless all go through it now, which is what the "TODO: Merge with the Win32
EmuThread and so on" in app-android.cpp was asking for.
Falls out of having one implementation instead of three:
- MainThreadFunc called graphicsContext->ThreadStart() right after
EmuThread_Start(), which already calls it, and ThreadEnd() right after
EmuThread_Join(), which already calls that too. So Windows and headless GL ran
CreateDeviceObjects()/DestroyDeviceObjects() twice per context, leaking a VAO
on the way in and double-deleting it on the way out. Dates back to 16206d18ab,
which moved those calls into EmuThread_Start/Join without removing the
originals.
- RECREATE_VIEWS is gone. Only the GL path ever posted it; Vulkan goes through
the same destroy/recreate without it and always has. Verified on device: views
survive pause/resume, and both a real rotation and a density change - each one
an activity destroy and recreate - lay them out again by themselves.
- The non-separate path is now named EmuThread rather than MainThread on
Windows. It's the thread that runs NativeFrame(), which is what
DebuggerThreading.md calls the emu thread, and the real main thread there is
the GUI message pump.
The loop keeps an explicit exit flag, as a shouldExit callback, rather than
relying on ThreadFrame() returning false. In VR we skip ThreadFrame() entirely
while the XR session is idle, and the session going idle is exactly what
backgrounding the app does - so the exit would never be observed, the render
thread would sit in that sleep forever, and the UI thread joining it would hang.
That is not hypothetical: it deadlocked a Quest 2 on the first try.
Also drops a dead EmuThreadState enum and an unused JNIEnv local in
app-android.cpp, both orphaned when the JavaGL JNI entry points went away.
Tested on a nubia Z2464N (Mali-G57, Android 15) on both backends: cold start,
booting a homebrew, pause/resume in the menu, in a settings screen and in-game,
rotation back and forth, a density-change activity recreate with the game live,
the pause menu, and exit to menu. ThreadStart/ThreadEnd log exactly once per
context, and every one of those cycles tears the EGL context down and builds a
new one.
Also tested on a Quest 2 with the vr flavor, which is what shook out the deadlock
above: cold start into a focused XR session at 72fps, backgrounding (render
thread joins in 27ms where it used to never return), resuming back into VR with
a fresh EGL context, and exiting from in-app. xrCreateSession runs on the render
loop thread, which is where the EGL context is current - VRBase.cpp builds its
graphics binding from eglGetCurrentDisplay/eglGetCurrentContext, so that ordering
matters.
Windows x64 debug and the Android gradle builds are clean.
Not converged, and not a small change: NeedsSeparateEmuThread() itself. GL needs
its API calls on the thread its context is current on, so somebody has to pump
ThreadFrame() there, whereas VulkanRenderManager owns its thread outright.
Getting rid of the virtual means teaching GLRenderManager to spawn and own its
own render thread, for every GL platform at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
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
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
EmuThread_Join drains the render queue on the calling thread. On Android
that is either the UI thread with no GL context (NativeApp.shutdown), or
the GL thread with a brand new context after the old one was lost
(displayInit the second time). The skip flag was only set once the emu
thread reached NativeShutdownGraphics, so everything queued before that
ran for real, against nothing or against the wrong context.
Add GraphicsContext::NotifyContextLost() and call it before the join in
both places. skipGLCalls_ is now atomic, since it's set from one thread
and read from another.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
If InitSurface failed, the thread returned but stayed joinable, so the next
surface was refused as "Already running" until the next pause. Tell a live
thread from a finished one with renderLoopRunning, and reap the latter.
Also make VulkanGraphicsContext::InitSurface clean up when it fails after
creating the draw context. Callers don't call ShutdownSurface on failure,
so draw_ (with its render manager) and the surface leaked, and the next
attempt overwrote the pointer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
requestExitVulkanRenderLoop went by renderLoopRunning, which the thread
only set once it got going. A pause landing between thread creation and
that point returned without signalling or joining, leaving a render thread
running while paused, and every later start refused as "Already running".
Go by whether there is a thread to join instead, set the running flag
before the thread is created, and let the requester reset exitRenderLoop
after the join rather than the thread itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Instead of the PSP's web browser, a dialog shows the URL the game wants
and opens it in the host's browser on X, or backs out on O. Either way the
game sees the browser closed normally. Platforms that can't open a URL
(the new SYSPROP_CAN_LAUNCH_URL) only offer to back out. Only plain
printable-ASCII http(s) addresses are handed over, and on Linux without a
shell.
What the firmware does (sceUtility_Driver, 6.61, plus
utility/dialog/htmlviewer): the HtmlViewer has its own state apart from the
other dialogs, so they don't block each other, and its calls return
WRONG_TYPE until one has started. The request size picks the 2.00 to 3.00
layout, and InitStart allocates 3.5MB of user memory (4.5MB with options
bit 0x400 from 2.70 on), failing with 800200d9 without it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TouchInput::id was used directly to index the global primaryButton[]
array (MultiTouchButton::Touch) and to shift pointer bitmasks
(PSPDpad/PSPStick/PSPCustomStick/GestureGamepad::Touch), guarded only
by a debug-only assert in one of the five call sites - a no-op in
release builds. input.id isn't always a small sequential slot in
[0, TOUCH_MAX_POINTERS): SDL assigns SDL_FingerID values directly,
Android pointer IDs can go up to 31, and UWP's TouchMapper allocates
one more slot (11) than TOUCH_MAX_POINTERS (10) and can also return -1
when it runs out of slots - all reachable through ordinary multi-touch
use, no malicious input required.
Also apply bounds check to the PER_GAME gesture config ints
(iDoubleTapGesture/iSwipeUp/Down/Left/Right) before indexing
GestureKey::keyList[] with them.
Additionally, minor cleanup on Android and moves the TouchMapper helper
out from UWP to InputState.h.