Commit Graph
458 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 bd94e8141f Android: Converge the GL and Vulkan lifecycles on a shared RunGraphicsLoop
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
2026-10-08 15:29:12 +02:00
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
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
Henrik RydgårdandClaude Fable 5.1 3cb6ada66a Android GL: Don't run stale GL commands when joining the emu thread
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>
2026-10-08 00:09:09 +02:00
Henrik RydgårdandClaude Fable 5.1 2c68ce0001 Android: Recover from a Vulkan render thread that exited by itself
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>
2026-10-08 00:09:08 +02:00
Henrik RydgårdandClaude Fable 5.1 99f65012f9 Android: Always join the Vulkan render thread on an exit request
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>
2026-10-08 00:08:19 +02:00
Henrik RydgårdandClaude Opus 5.5 5f261bd7ff Utility: Stand in for the HtmlViewer, offering to open the page in a browser
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>
2026-09-25 12:50:23 -06:00
Henrik Rydgård 04a6166c90 Android: Remove some unused code 2026-08-31 11:02:32 +02:00
Henrik Rydgård 9315c0953a GamepadEmu: bounds-check touch pointer IDs before use
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.
2026-08-12 09:47:13 +02:00
Henrik Rydgård a44c2c0bde Replace System_SendDebugOutput with a registered callback
I normally try to avoid registrations when not needed, but in this case
only headless uses this, so it's motivated.
2026-08-11 22:36:47 +02:00
Henrik Rydgård 092ab42fe7 Clean up ugly usage of the g_nativeLibDir global. Now it's local in VulkanLoader. 2026-08-06 18:12:04 +02:00
Henrik Rydgård 16206d18ab Simplify the EmuThread API again 2026-08-05 09:48:51 +02:00
Henrik Rydgård 905650921e Android hang fix when switching backends, buildfix 2026-08-05 00:59:33 +02:00
Henrik Rydgård 4add25e6c7 Buildfixes, bump gradle 2026-08-05 00:15:56 +02:00
Henrik Rydgård 5eb4772644 Minor EmuThread refactor 2026-08-05 00:15:56 +02:00
Henrik Rydgård be922eca62 Make OpenGL render thread shutdown more robust. 2026-08-04 16:44:55 +02:00
Henrik Rydgård ccff34b431 Reuse the AndroidJavaGLContext as the new generic OpenGLGraphicsContext 2026-07-26 20:07:14 +02:00
Henrik Rydgård 3784bbe0e6 Delete the AndroidVulkanContext, replace with the generic VulkanGraphicsContext 2026-07-26 19:10:46 +02:00
Henrik Rydgård 3eb056ad86 Move Common/GraphicsContext.h to Common/GPU/GraphicsContext.h 2026-07-26 13:58:17 +02:00
Henrik Rydgård 8020b60dda Use the new common RunMainLoop on Android 2026-07-26 13:42:16 +02:00
Henrik Rydgård 29edec7684 Move calling ThreadStart and ThreadEnd into the EmuThread wrapper 2026-07-26 11:54:50 +02:00
Henrik Rydgård ea3a2a2e84 Use the common EmuThread in the Android backend 2026-07-26 11:54:50 +02:00
Henrik Rydgård 7f218acc53 Change the GraphicsContext interface to be more consistent 2026-07-26 10:22:00 +02:00
Henrik Rydgård c2c0fa3634 Remove the AndroidGraphicsContext middle-level class 2026-07-26 10:21:59 +02:00
Henrik Rydgård 85f8715660 Break the circular dependency caused by putting EmuThread in core using an interface 2026-07-24 15:31:42 +02:00
Henrik Rydgård cb1b4067f7 Small optimizations in app-android.cpp 2026-07-24 15:31:42 +02:00
Henrik Rydgård b2c5fdc39f Make EmuThreadFunc match Android's EmuThreadFunc 2026-07-24 15:31:42 +02:00
Henrik Rydgård 02017ad41e Fix name collision between functions 2026-07-24 15:31:42 +02:00
Henrik Rydgård 8b05f703a4 Clean up unused parameters 2026-07-24 15:31:42 +02:00
Henrik Rydgård 906d877019 Android: Remove some state and a race condition 2026-07-24 15:31:42 +02:00
Henrik Rydgård 86c8e9ae68 Minor code cleanup 2026-07-24 15:31:41 +02:00
Henrik Rydgård 16c7ec390f Headless: Start taking logic out of the "HeadlessHost"
Also update some agent files
2026-07-21 11:01:48 +02:00
Henrik Rydgård a59637975a Call the new command line parser from all backends 2026-07-20 12:02:07 +02:00
Henrik Rydgård 26313b6314 Log improvements 2026-07-15 12:03:05 +02:00
Henrik Rydgård 993c3a9c67 Avoid accurately computing disk space on iOS and Android (where it can be slow). Add a setting. 2026-07-14 22:22:29 +02:00
Henrik Rydgård 6d98293be9 Hide orientation controls on large Android 17 devices, since they won't work. 2026-06-15 10:37:58 +02:00
Henrik Rydgård 697179c8cd Add some plumbing for the Android 17 local network access permission 2026-06-15 09:45:09 +02:00
Henrik Rydgård 01d65ada40 Merge pull request #21760 from misantronic/feature/achievementshost-cli-override
Android: add achievements host override receiver
2026-06-01 11:05:22 +02:00
david.schkalee d477afa9b2 Android: add achievements host override receiver 2026-06-01 01:44:04 +02:00
Henrik Rydgård e8e357e360 Implement the "Mix audio with other apps" setting on Android. 2026-05-31 19:08:45 +02:00
Henrik Rydgård ed274c8a7e Just some lint fixing 2026-05-13 11:37:26 +02:00
Henrik Rydgård e23d866a95 Plumb through an error code for missing activity. Will later be used to improve the UI. 2026-05-11 10:52:36 +02:00
Henrik Rydgård fead647304 Android: Improve insets behavior on older devices without camera cutouts 2026-03-14 22:53:17 +01:00
Henrik Rydgård 68a59e42cd Fix a few bugs in app-android.cpp 2026-02-26 10:44:21 +01:00
Henrik Rydgård a03e112a80 Android: Avoid a few string copies 2026-02-26 10:37:12 +01:00
Henrik Rydgård b7ad74d309 MetaFileSystem: Turn another argument into string_view 2026-02-25 00:52:25 +01:00
Henrik Rydgård e3e6148459 Add a way to query the installer name on Android. 2026-02-14 10:48:40 +01:00
Henrik Rydgård 03c901d0cc Prepare for multiple gesture mapping zones 2026-02-04 15:01:06 +01:00
Henrik Rydgård dccca3c441 Android: Track device disconnection 2026-01-25 23:02:12 +01:00
Henrik Rydgård 3c4adc3e66 Fix a number of Android Studio lint warnings, a bad jni return type and an memory leak 2026-01-21 18:43:47 +01:00