Commit Graph
497 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 61fa52d826 Android: Scale down the shortcut icon to fit the result intent
The icon was padded to a full-width ARGB square, so a big ICON0 made the
result too large for the binder transaction. finish() threw, we caught it,
and the next finish (the back gesture) crashed with "Could not copy bitmap
to parcel blob". Scale it to launcher icon size, and if finishing still
fails, retry without the icon.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 02:02:44 +02:00
Henrik RydgårdandClaude Opus 5.5 0829c442d0 Android: Don't prune grants from a fallback config, keep shortcut order
NativeInit falls back to the default memory stick when the one in memstick_dir.txt can't be read (on
Android 9 and older, /sdcard), and an old ppsspp.ini there would make the user's grants look unused.

Also release before re-taking folders, since a take can make the system prune, keep going when one
release fails, save the shortcut list when only its order changed, and re-take the Remote ISO shared
folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 15:29:26 +02:00
Henrik RydgårdandClaude Opus 5.5 8e146f4c24 Android: Keep folder and shortcut grants, re-take the folders we use
Pruning now only runs when the config was really read: when the memory
stick isn't reachable at startup (an unmounted SD card, or right after
boot), NativeInit falls back to defaults and the recent list is empty, so
every recent game's grant looked unused and was released.

Grants for the memory stick, the Games tab folder and pinned folders are
re-taken at every startup. That moves their persisted time up, so they're
the last the system drops if we ever reach its limit. The new time is
written out with the next grant change, which is also the only time the
system prunes.

Home screen shortcuts can't be enumerated, so the 64 most recently
created or launched are remembered in a new [Shortcuts] section of
ppsspp.ini, and their grants are kept. ShortcutActivity runs without the
config loaded, so it queues them in SharedPreferences until the next
startup saves them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 13:54:31 +02:00
Henrik RydgårdandClaude Fable 5.1 d7bac55348 Android: Release old unused file grants before the system starts dropping ours
An app can hold 512 persisted URI grants (128 before Android 11). Over
that, the system drops the oldest, and ours is usually the memory stick
folder. We take a grant for every file picked one at a time - games loaded
through the file picker, shortcut targets, sound effects - and never
released any.

At startup, log what we hold, and past half the limit release the oldest
single-file grants that neither the recent list nor a setting points at.
Folder grants are always kept. Home screen shortcuts need their file's
grant and can't be enumerated, which is why this only acts when it has to
and goes oldest first: the files it takes are ones the system would have
taken too, after the folder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 13:54:31 +02:00
Henrik RydgårdandClaude Opus 5.5 be4bdf3590 Android: Show the game on the TV again after the system dismisses it
Android dismisses a Presentation by itself on some display changes, like
a mode switch right after plugging in. We kept the dismissed one, so the
TV went back to mirroring until it was replugged. Its dismiss listener
now drops it and checks again, and so does a change to any display other
than the device's own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:01:03 +02:00
Henrik RydgårdandClaude Opus 5.5 04ae3fea5b External display setting: Show the game, or mirror the screen
Graphics > "External display" on iOS and Android. "Show the game" (the
default) is the TV mode, "Mirror the screen" leaves the TV to the system.
A change applies right away. It's disabled on backends that can't draw to
a second swapchain.

On Android 12 and up, a second screen that's part of the device (like a
dual-screen handheld's) isn't used, so the game doesn't move there by
itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:31:07 +02:00
Henrik RydgårdandClaude Opus 5.5 5fbd134a7d Android: Show the game on a second display, with the controls on the phone
A presentation display (HDMI or USB-C video out, some wireless displays,
or the simulated ones in Developer options) gets a Presentation with a
SurfaceView, whose surface becomes a Game output, when the backend is
Vulkan. Otherwise Android keeps mirroring.

The surface mustn't be touched after surfaceDestroyed returns, so the
output is removed with the new Native_RemoveOutputAndWait(), which
returns once the render thread has let go of it (or right away if the
graphics are down). The UI drawn on an output now leaves out the main
swapchain's pre-rotation, as the game already did.

"In menus on %1" gets the device name from Settings.Global.DEVICE_NAME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:31:07 +02:00
Henrik RydgårdandClaude Opus 5.5 fbf0cc2df4 Android: Detect the accelerometer and gyro instead of assuming them
SYSPROP_HAS_ACCELEROMETER was true for every phone and tablet, whether or
not it had the sensor. The activity now tells the native side what the
sensor manager found, which also answers the new SYSPROP_HAS_GYRO. VR
headsets still report neither. Every other platform returns false for the
gyro for now.

The "Use gyroscope" checkbox uses the new property instead of waiting for
gyro data to show up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:20:20 +02:00
Henrik RydgårdandClaude Opus 5.5 f84e63f753 Tilt: Steady the tilt input with the gyroscope where there is one
Tilt used the raw accelerometer as the gravity vector, which jitters with
sensor noise and jumps when the device is shaken. Now, when gyro data is
coming in, a complementary filter integrates the gyro rates and only pulls
the result toward the accelerometer with a 0.5 s time constant, trusting
the accelerometer less the further it reads from 1g. The fused vector goes
through the same ProcessTilt path, so calibration and sensitivity work as
before. Without a gyro, nothing changes.

The fusion lives in TiltEventProcessor behind NativeGyroscope(), so other
sources (DualSense) can feed it too. Android registers TYPE_GYROSCOPE next
to the accelerometer. A "Use gyroscope" checkbox (on by default, per game)
appears in tilt setup once gyro data has been seen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 10:45:37 +02:00
Henrik RydgårdandClaude Opus 5.5 df76bae5dc Android: Read the Args extra also when launched with a URI
- Fixes #18528

Launchers typically open the game with ACTION_VIEW and a content:// URI, so
they could never pass options before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:30:19 +02:00
Henrik RydgårdandClaude Opus 5.5 d6985d81eb Android: Simplify joystickAxis and queryGameInfo, drop dead JNI calls
joystickAxis copies the axes in batches on the stack instead of allocating
per event, and bounds the count by the arrays it got. queryGameInfo's cache
is a local, in a helper so it's gone before the thread manager is torn down.

Removes Android_GetFreeSpaceByFilePath (never called) with its Java side, and
isLandscape, which only existed to call NativeGetAppInfo early.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 01:36:35 +02:00
Henrik RydgårdandClaude Opus 5.5 a09f59910b Android: Fix JNI edge cases, read Java strings as real UTF-8
- Scoped storage no longer flips on while no activity is registered: whether
  legacy storage was preserved is fixed per install, so it's asked once.
- Free space reads as unknown (-1), not 0 bytes, without an activity.
- Strings from Java (file listings and info, crash history, the camera list,
  the input debug string) go through GetJavaString, so names with characters
  outside the BMP aren't mangled, and a null string can't crash the crash
  history.
- Requests (inputbox, browse_*) whose command can't be delivered are failed
  instead of left waiting forever.
- A font that fails to load uses the default typeface (-1) instead of font 0,
  and is retried later if there was no activity yet. Missing exception checks
  added in the text renderer.
- Method IDs and the storage callbacks are looked up once in JNI_OnLoad.
- Unknown permissions are logged correctly.

Also removes what was left of Moga support, the unused backbuffer format and
a stray MessageBox macro, and tidies System_GetCameraDeviceList.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 01:29:18 +02:00
Henrik RydgårdandClaude Opus 5.5 363fe679c2 Android: Make location an optional feature
The location permissions imply android.hardware.location as required,
on top of the .gps and .network subfeatures we already marked optional.
Also skip the permission prompt on devices without location hardware.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 19:49:12 +02:00
Henrik RydgårdandClaude Opus 5.5 1d7458c4f9 Android: Don't ask for microphone permission on devices without one
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 19:28:12 +02:00
Henrik RydgårdandClaude Opus 5.5 beaae7016f Android: Keep the orientation setting on large Android 17 screens
Android 17 ignores fixed orientations on sw600dp+ displays, but exempts
games, and we declare appCategory="game". Tested on an Android 17 tablet
emulator: with the category, Portrait is honored (letterboxed); without it,
the system logs "Ignoring requested fixed orientation". So stop hiding the
rotation setting there, and drop the smallestScreenWidthDp plumbing that
only existed for that check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 20:44:07 +02:00
Henrik RydgårdandClaude Opus 5.5 d2e9e87242 Android: Ask for the local network permission before starting file upload
Part of #21585

Android 17 apps need ACCESS_LOCAL_NETWORK to serve other devices on the LAN.
The upload screen now asks for it when it opens, starts the web server once
it's granted, and shows a warning and a retry button if it's denied.

The permission only exists from API 37, so check for that rather than 35.
On Android 15 and 16 it reads as denied, which left Remote ISO's connect tab
stuck behind a permission request that could never succeed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 20:44:04 +02:00
Henrik RydgårdandClaude Opus 5.5 3afb17f7d4 Android: Don't leave room for system bars we hid
Before Android 11, decorFitsSystemWindows(false) sets LAYOUT_STABLE, and
the system bar insets then keep the space of hidden bars. So in
landscape, where the status bar is always hidden, the UI had an empty
band at the top (seen on a Galaxy S8 and a Pocophone F1). Count only
the bars we show, and reapply the insets after changing that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:59:06 +02:00
Henrik RydgårdandClaude Opus 5.5 56580581e0 Android: Check for Java exceptions after content URI calls
ContentUri catches Exception, but an Error got through, and none of the
native wrappers checked. A thrown int call returned 0, which reads as
SUCCESS, and the exception stayed pending, so the next JNI call aborted.
copyDocument/moveDocument are API 24/25 while the folder browser is
offered from 21, so those threw NoSuchMethodError there - now they return
a failure so the callers fall back.

Also convert non-URI names to Java's modified UTF-8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:19 +02:00
Henrik RydgårdandClaude Opus 5.5 bfa0ef9d1f Android: Small crash fixes in the activity and shortcut creation
- Catch takePersistableUriPermission failing in ShortcutActivity, as
  DocumentResultProxyActivity already does. Some pickers don't grant it.
- Treat a cancelled permission request (empty results) as denied.
- Handle getCacheDir() returning null, like getFilesDir().
- Don't let the input device debug string, read from the render thread,
  throw ConcurrentModificationException into native code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:19 +02:00
Henrik RydgårdandClaude Opus 5.5 51a799623c Android: Fix GPS and infrared crashes on devices without them
requestLocationUpdates() throws IllegalArgumentException for a provider
that doesn't exist (WiFi-only tablets, TVs, Chromebooks), which crashed
any game opening the GPS. Register each provider separately.

Without an IR blaster, the IR service is null, and InfraredHelper was
created anyway, so a sceSircs call NPE'd. Also catch transmit() failing,
like the SecurityException on API 21/22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:19 +02:00
Henrik Rydgård 8d8009840e Merge pull request #22511 from hrydgard/android-inputbox-fixes
Claude code review: Android inputbox fixes
2026-10-12 13:20:25 +02:00
Henrik RydgårdandClaude Opus 5.5 7678336e28 Android/VR input: Lock the VR key state, fix device tracking and release
pspKeys and pspAxis were written from the input thread and the render
thread without a lock. Entering VR camera adjust "released" held keys by
sending PSP button codes as keycodes, which released nothing; use
ControlMapper::ReleaseAll instead.

Input devices are now matched by ID. InputDevice objects are replaced after
device changes, so reference comparison registered the same device again.
Removing a device now releases what it was holding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:56:32 +02:00
Henrik RydgårdandClaude Opus 5.5 2aa3e8d6d3 Android: Always answer native requests
An input box whose show() threw, a second input box open at onDestroy, and
browse commands dropped for a destroyed activity or failing in
processCommand were never answered, leaving the native side waiting (a
game's OSK forever). The image proxy also crashed the app on a picker
result without a URI, a SecurityException or an OOM.

Input boxes now answer exactly once, instead of a success followed by an
ignored failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:37:19 +02:00
Henrik RydgårdandClaude Opus 5.5 6071983094 Android: Mask password input boxes, don't log entered text
The passwordMasking flag of System_InputBoxGetString() never reached Java,
and both sides logged the entered text, including RetroAchievements passwords.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:29:47 +02:00
Henrik RydgårdandClaude Opus 5.5 6cf4872e78 Android: Hand microphone input over on the CPU thread, like the other hosts
The Oboe input callback used to call Microphone::addAudioData directly,
writing PSP memory from the audio thread. Now it only pushes samples
into a SampleRing, and pollRecording drains it on the CPU thread.
Starting and stopping still go through Java, which asks for permission.

Every host with a microphone polls now, so sceUsbMic drops what only the
push path needed: QueueBuf's recursive mutex, restarting the mic on a
device change, and the startMic parameter nobody read. Also removes the
unused System_AudioRecordingState and Microphone::isNeedInput.

The Java microphone_command branch didn't return true, so every command
logged "processCommand failed".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:49:21 +02:00
Henrik RydgårdandClaude Opus 5.5 77a6abfe6e Add System_ShareFile, for handing a file to another app
Offers a file through the system share sheet, for things like sending a
zipped savestate to a friend. Android for now (SYSPROP_SUPPORTS_SHARE_FILE):
content:// paths from a picked folder are shared as they are, and plain paths
through a FileProvider. Either way the receiving app can read just that file.
With no MIME type given, it's guessed from the extension.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:59:42 +02:00
Henrik RydgårdandClaude Opus 5.5 d35cdf8fcf Android: Track the activity that owns the native side by identity, and log changes
Replaces the instance counter with a weak reference to the current activity,
and says in the log when an activity takes over, and when a replaced one skips
something because the native side isn't its anymore.

The render loop state is static now, with the activity whose surface it runs
on: there's one render thread. An activity only stops the loop if it runs on
its own surface, and one that finds another activity's loop still running
logs an error and stops it before starting its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:59:16 +02:00
Henrik RydgårdandClaude Opus 5.5 2123e7fdac Android: Tie emulation and audio to visibility, not focus
onPause used to join the render thread and stop the audio, so anything taking
focus while the game stayed on screen (a permission dialog, a share sheet, the
other app of a split screen before Android 10) tore down the whole graphics
context and silenced the game. Emulation, audio, sensors and input listeners
now follow onStart/onStop. The render loop itself already follows the surface;
onStop joins it too, for devices that keep a stopped activity's surface.
The camera stays on onPause/onResume, as it goes to the focused app.

A finishing activity lets go of the render thread in onPause, since a quick
relaunch creates the next activity before this one's onStop, and that onStop
then leaves the audio and location updates alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:45:27 +02:00
Henrik RydgårdandClaude Opus 5.5 87353cfd01 Android: Fix activity state lost on recreate, and a destroyed activity kept alive
- Initialize() runs for the first activity only, but set up per-activity
  fields: after any recreate, audioManager was null, so audio focus was
  never taken or given up again, and sustained performance mode was off.
- The static camera helper kept the first activity's display. It now gets
  the application context.
- An activity backed out of (or destroyed by the system) with the native
  side left running stayed referenced by it until the next one registered,
  and native code could still post commands to it. It now lets go, and a
  command arriving at a destroyed activity is dropped (an input box request
  answered as cancelled).
- An open input box is dismissed in onDestroy, instead of its window
  leaking and its request never being answered.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 15:31:24 +02:00
Henrik RydgårdandClaude Opus 5 a2869a47cd Android: Re-report the surface when the display density changes
Now that density is in configChanges, a density change no longer recreates
the activity - but it also doesn't resize the surface, so no surfaceChanged
follows it. The DPI is only recalculated in backbufferResize, so the UI kept
the old scale until the next real resize or a trip through the background.

Re-report the surface we already have when the density has moved. The native
side needs no change: Recalculate() already returns true when the dp size
changes, which a density change does on its own.

Also make the log line say which size changed, since the pixel size now stays
put in this case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 21:51:35 +02:00
Henrik RydgårdandClaude Fable 5.1 114512774d Android: Keep the native smallestScreenWidthDp current across config changes
It was passed once at init, and decides whether we can restrict the
orientation on Android 17+ large screens. Now that a fold or unfold no
longer recreates the activity, forward the new value from
onConfigurationChanged. Untested on a large screen device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik RydgårdandClaude Fable 5.1 be6ff0bc9b Android: Trim SizeManager down to the surface callback, rename to NativeSurfaceManager
Now that rotation doesn't recreate the activity, the class is the
SurfaceHolder.Callback plus the hardware scaling logic, and the rest had
outlived its purpose:

- The deferred-surface-while-paused workaround. Also what started the
  render thread twice on resume.
- The "bad orientation" workaround, which called recreate().
- The deprecated system UI visibility listener, which only chose between
  getMetrics and getRealMetrics, and the view size overrode that whenever
  it mattered anyway.
- The coalescing of display measurement updates, and updateDpi, which
  updateDisplayMeasurements does itself.

Before the first layout the display size now comes from getRealMetrics.
It only feeds the first-run config defaults.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik RydgårdandClaude Fable 5.1 d0e45b94d9 Android: Handle rotation and window resizing without recreating the activity
Every rotation, fold or window resize destroyed the activity and with it
all of graphics, then built it all up again. Declare those config changes
handled, so they arrive as a surfaceChanged and go through the same resize
path as a desktop window resize.

That path needed three things it didn't have on Android:

- The GPU_DISPLAY_RESIZED message, left out for Android since 2020 when
  DeviceRestore covered every size change.
- A fixed (hardware scaled) surface size is now recomputed on each in-place
  surface change, since surfaceCreated no longer runs on a rotation.
- A 180 degree flip keeps the size but changes the surface transform, so
  the display rotation is passed along and a change in it is a resize too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik RydgårdandClaude Opus 5 afedbab578 Android: Remove LifeCycle.java, keep the logging
It was a debug aid that asserted the activity callbacks arrive in the order
Android promises. That has served its purpose - nothing has tripped it in a long
time, and the lifecycle work this sits on top of has been exercised hard.

What it also did, though, was log every callback, and that is worth keeping: the
begin/end pairs are what makes a bad startup or a surprise recreate legible in a
logcat. So the tracker goes and the Log.i calls stay, at the same points, under
this class's own tag instead of LifeCycle's.

onStart and onStop now only log, so they lose the no-native-binary guard. There
is nothing left in them to guard, and if the library is missing is exactly when
you want to see the callbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 937022aa8e Android: Don't log every mouse movement
Moving a mouse logged two lines from Java and one at INFO level from C++
for every motion event.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 726c98198d Android: Mark the last two JNI-called Java methods @Keep
renderText and getNativeCrashHistory are only called from native code,
like their neighbours that do have the annotation. Harmless today since
minification is off, but it would strip or rename them the day it's on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 4a3ba23820 Android: Forget a key when it's released
pressedKeys is there so Disconnect() can release what's held, but keys
were only ever added to it, so it listed every key pressed so far.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 a77eba1554 Android: Create the TextRenderer paint only once
TextRenderer.init runs in every onCreate and replaced the static Paint. On
the OpenGL path the emu thread survives the activity and can be in the
middle of measuring or drawing text with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 26d533904e Android: Hand out the same font ID for the same font file
Each TextDrawerAndroid allocates its fonts afresh, and one is created
every time graphics come up. allocFont gave every request a new ID and map
entry, and nothing ever calls freeAllFonts, so the map grew by a few
entries per resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 b16693cf48 Android: Survive long enough to show the missing native library message
When the library fails to load, onCreate shows a dialog and returns before
setting anything up. But onResume, onWindowFocusChanged and the rest still
ran, dereferencing the missing SizeManager or calling into the missing
library, so the app crashed before anyone could read the dialog. Make them
all check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 d887e4a8ae Android: Release the camera if starting it fails
If startCamera threw after Camera.open (no usable preview size, say),
mIsCameraRunning stayed false, and pause() only releases a running camera.
So it was held until the process died, locked for every other app. Release
it on failure, and before opening it again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 cec72d46c5 Android: Stop location updates while paused
Nothing turned the GPS off when the app went to the background, so a game
that had opened it kept it running until the process died. Pause and
resume it like the camera. A start request that arrives while paused (as
the one from the permission dialog does) is deferred to resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 90c7cd2a03 Android: Release all pointers on ACTION_CANCEL, not just the first
ACTION_CANCEL ends the whole gesture, and its action index is always 0.
We only sent an up for that pointer, so with more than one finger down
when the system took over the gesture, the others stayed pressed on the
native side - stuck on-screen buttons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 68ae147033 Android: A replaced activity must not start or stop the render thread
There is only one render thread, shared by whichever activity is
current. After a finish() and a quick relaunch, the old activity's surface
is destroyed about a second into the new activity's life, and joining the
thread from surfaceDestroyed then stopped the new activity's rendering.

Found by forcing the finish-then-relaunch order with a test hook: without
the previous commit the process died in NativeApp.shutdown(), with it the
app survived but went black until this was added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Fable 5.1 9cae1a6653 Android: Don't let a late onDestroy shut down a newer activity's native state
After finish(), onDestroy may arrive after a new activity has already been
created and resumed, if the user relaunches quickly. The new one sees
initialized still set and adopts the native state. The old one's onDestroy
then shut down audio and called NativeApp.shutdown(), deleting the
graphics context the new render thread was using.

Number the activities, and leave the native side alone in onDestroy if a
newer one exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Fable 5.1 6a712a927a Android: Don't create the static location and IR helpers from the activity
They're static and created once, and the LocationManager they look up
keeps the context it came from, which pinned the first activity for the
life of the process. Use the application context.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Fable 5.1 dc2c165084 Android: Unregister the power saving settings observer
PowerSaveModeReceiver registered a ContentObserver that captures the
activity, and destroy() only unregistered the broadcast receiver. The
system keeps registered observers alive, so every recreated activity
stayed in memory. Found with a heap dump: 8 of 9 live activities after
some rotating were held this way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 18:54:46 +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 5b677d93a1 Android: Don't send the launch parameter to the native side twice
On a cold start, onCreate passed the intent's shortcut parameter to
NativeApp.init and then also posted it as a "shortcutParam" message, which
is only meant for when the native side is already up. For a game shortcut
the second boot request was rejected with an error. For the Args extra,
the raw command line got booted as if it was a game path, so something
like --loglevel=4 applied fine and then popped up a load failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 00:09:09 +02:00