The old disarm handled the integer instructions but only a fraction of VFP
and NEON, and got several of those wrong. The new one decodes ARMv7-A ARM
state with VFPv4 and NEON, printing UAL with upper case mnemonics.
The new ArmDisasm test runs every ARM emitter function through it, two
examples each. Tools/arm_disasm_check.py compares the test's output with
llvm-mc; they agree on all of it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rewrites most of DisArm64.cpp. Previously missing: test-and-branch,
exceptions other than brk, hints and barriers, adc/sbc, conditional
compare, extr, literal loads, exclusives, unscaled and FP register
offset loads, ldpsw, SIMD structure loads/stores, scalar pairwise,
fixed-point conversions, fccmp, most two-reg misc ops, modified
immediates, smov/umov/dup from GPR, and the scalar shift forms.
Fixed: sp printed as x31, unsigned load offsets sign-extended, ldrsb and
friends shown as stores, adrp ignoring the page, double precision
1-source ops shown as single, fcmpe shown as fcmp, smulh/umulh, the
bitmask decode for 64-bit elements, and fpcr shown as fpsr. Common
aliases are used now (cset, mul, tst, cmn, lsl/asr/ubfx, sxtw...).
TestArm64Disasm runs two examples of every emitter function through it.
The expected strings were verified with Tools/arm64_disasm_check.py,
which compares all 833 instruction words against llvm-mc.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Android headless build only did software rendering. Vulkan takes the
offscreen path desktop headless already uses, and OpenGL ES renders into an
EGL pbuffer, so both run from adb shell with no window. Assets are found next
to the executable, like on desktop, instead of in an APK path that no longer
exists.
frametests.py --adb pushes the binary, assets and dumps to a device, runs each
dump there with Vulkan and GLES, and compares the screenshots with the host's
Vulkan render using Tools/image_compare.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They are meant to be separate shared libraries that driver.cpp loads into the
driver's namespace by name (ext/libadrenotools builds them that way, and so
does the gradle build). Linked into an executable, file_redirect_hook.c's
global fopen() takes over every fopen in the process, including the one in
libc's own startup, and hook_fopen() calls fopen() again forever. That hung
ppsspp_headless (and the unit test executable) before main().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>
GetJavaString() returned Java's modified UTF-8, so input like emoji
reached C++ as invalid UTF-8. Commands to Java now get converted the other
way. Request IDs are made on several threads, and the permissions map was
written on the Java UI thread and read on the render thread. The frame
command debug log no longer prints params, which could hold a password.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Zips the given files (content:// paths included) into a folder of their own
in the app's cache and offers the zip through System_ShareFile. The receiving
app reads it whenever it likes, so it can't be deleted right away: each share
deletes the folders of earlier shares once they're an hour old.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
Now that EGL is ours rather than GLSurfaceView's, the swap interval can be set,
so turning vsync off (and fast-forward without skipping flips) works on Android
GL too. Not in VR, where OpenXR presents, and not with a config whose minimum
swap interval is 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
- 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>
The C++ paths draw and sample four pixels at a time in vector lanes,
while a JIT function only does one, and with a JIT function in use the
span and quad paths were skipped. On x86 the JITs had become slower:
Wipeout took 13% more wall time and 25% more CPU with them, God of War
3% more. They were also a second implementation of every GE rule, and
ARM never had them.
The SoftwareRendererJit setting goes with them. RasterizerRegCache.h
keeps only the types the pixel and sampler functions take, as
RasterizerTypes.h.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Oboe uses AAudio on Android 8.1+ and falls back to OpenSL ES on older
devices. Output and microphone input both go through it. The mic now gets
resampled by Oboe from the device's rate to whatever the game asks for.
New: when a stream is disconnected (headphones unplugged, Bluetooth device
going away), it's reopened on the new default device instead of going
silent.
Oboe 1.11.0 is added as a submodule, built through ext/oboe-build for CMake
and as a module in Android.mk.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
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>
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>
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>
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
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>
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>
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>
It's written by the thread that brings graphics up and down, and read by
every input function on the UI thread. The comment also claimed it was
only used with OpenGL, but the Vulkan path sets it too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
An intent arriving while we're running was passed on as a game path,
whole. That was right for a shortcut or a file association, but the Args
extra carries a command line, so its options got booted as a file.
Run it through the same parsing as at startup. The options can't be
applied anymore and aren't, but parsing them is what separates them (and
their arguments) from the game to boot. If there is none, do nothing.
This also unescapes a quoted path the same way NativeApp.init does, where
before only the quotes were stripped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
We never said either way, so the system treats it as resizeable only by
virtue of the target SDK (RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION). Samsung
DeX wants it spelled out before it offers free resizing and maximize, and
a user reports being stuck in a small fixed window there. Not verified on
DeX.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
registerCallbacks took a new global ref to the activity on every onCreate
without releasing the previous one, and unregisterCallbacks only runs on a
real shutdown. Since rotation and resizing recreate the activity, each of
those leaked the old one, view hierarchy and all.
Releasing it means other threads can no longer use the raw global ref,
which had been copied into AndroidStorage and TextDrawerAndroid and was
read from the emu thread and IO threads. They now take a local ref under a
mutex (Android_GetActivity, and the equivalent inside AndroidStorage for
the shortcut activity's sake), so a call in flight keeps its activity
alive while the UI thread swaps in the new one.
The "no activity" bail is a null check on that local ref rather than a
separate HasActivity() test beforehand. Between such a test and the call
the UI thread can swap the activity out, and we'd hand null to a Java
method that wants a Context. The one remaining HasActivity() caller never
passes the activity on, so it has nothing to race with.
The ContentUri class ref and method IDs are now only looked up once,
instead of leaking a class ref per activity.
Headless and the unit test executable link Common but not app-android.cpp, so
they get a stub for this the same way they already do for getEnv and findClass.
Without it the Android CI build fails at link time, where a normal gradle build
is perfectly happy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
Local refs are only freed automatically when a native method returns to
Java. The content URI functions mostly run on threads we attached
ourselves, where nothing ever freed the strings they created - two per
file open, one per exists check and so on. From Android 8 that's a slow
leak; before it the table has 512 slots and overflowing it aborts.
Add JNILocalFrame (PushLocalFrame/PopLocalFrame) and use it in all of
AndroidStorage.cpp, in the camera device list, and free the name string
in findClass.
Also: Android_GetFileInfo never released its GetStringUTFChars buffer, a
native leak on every call, and Android_ListContentUri called
GetArrayLength on a null array if the Java side returned none.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>