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>
--loglevel, -d and -v are applied to the channels directly, and the next
config save wrote them to [Log], permanently replacing the user's levels.
The other command line options avoid this with DoNotSaveSetting.
The debugger had the same problem and a flag for it. Generalize that one
(NotifyChannelsChangedByDebugger -> DoNotSaveChannels) and set it here too.
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>
The same lerps as the GE-exact Bezier path, four at a time. At each v step,
four control columns per vector share the v blending factors; then each row
is evaluated at four u steps, each lane with its own six blending factors.
A vector's u steps can straddle a segment boundary, so the row's points are
gathered per lane, or splatted when all four are in one segment.
The unit test compares it bit for bit with the scalar code, including
tessellations that split a vector across segments. The GE-exact spline
benchmark runs about twice as fast on an M1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CreateDir's "repair" of existing directories ran for every component
CreateFullPath walked through, so creating any directory under the
user's home made the home directory itself group-writable (775). That
breaks zsh's compinit ("insecure directories") among other things.
Only repair the permissions of the target directory itself, when it
already exists.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four of the GE's lerps at once: both operands go to 16-bit fixed point at
the larger exponent by scaling with a power of two and truncating, then the
integer lerp (split into byte halves so both multiplies fit 16 bits), then
back. The patch's four control columns run in the four lanes at each v
step, then four u steps of a row at once, with the row's first level of
lerps converted once per row. Lanes whose exponents are tiny take the scalar
lerp, and the normals' cross product stays scalar for now.
The scalar code stays as the reference (g_splineGEScalar), and the unit test
compares the two bit for bit on random control points. The GE-exact Bezier
benchmark goes from 3.4 to about 8.8 Mverts/s on an M1; the float SIMD path
does about 100.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Written while testing the activity lifecycle work on a Galaxy S8 and a Quest 2.
Mostly about ways the tooling answers a question you didn't ask.
The expensive one: versionCode is the git commit count, so installing a feature
branch over a longer branch fails with INSTALL_FAILED_VERSION_DOWNGRADE, which
-r does not override. The device keeps running the previous build, and if the
install output went through a pipe the failure is never seen. An A/B where both
installs failed that way agrees with itself perfectly and means nothing.
Also: dumpsys meminfo counts activities that are garbage but not yet collected,
so the obvious reading looks like a leak whether or not there is one - trim and
re-read until it settles; how to force an activity recreate over adb when the
device won't rotate on command; why screencap is black on a Quest and what to
read instead; and how to tell whether a device exercises the content-URI path at
all before concluding a run covered it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
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>
Each TextDrawerAndroid took a global ref to the TextRenderer class and
never released it (the release had been commented out as crashing), plus
leaked the local ref it was made from. A TextDrawer is created every time
graphics come up, so that was one of each per resume.
Look the class up once and keep it. Also initialize the method IDs, which
IsReady() read uninitialized if the class was missing, and don't walk into
a null array if renderText throws.
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>
The software renderer drops a color test that only rejects black under blending that adds to the
destination, since rejecting black then only skips adding nothing. With dithering on it doesn't:
a rejected pixel keeps the destination, an accepted black one gets the dither added. Ridge Racer 2
(20839) adds a glow that way and came out one step darker wherever the dither matrix was negative;
Burnout Legends' bloom (16119) too. Fog and logic ops would change the destination the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At the end of the 256 KB list buffer, playback jumped back to its start and kept writing, so when
the GE was more than a lap behind, the next lap overwrote commands it hadn't reached. Warriors
(14660) lost half its draws, and GTA LCS (15149) some. Finish the list, wait for it, and start a
new one instead, as the PSP replayer does. Both now match the PSP's replay exactly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Dumps recorded before savedContextVersion 1 hold PPSSPP's old context layout, with the matrices
as raw floats, but GEState::Restore picks the layout from savedContextVersion, which only a
savestate sets. Playback restored them as the new layout, so the matrices became garbage and
the scene vanished (4140 drew only its HUD). Tell the layouts apart by the END the new one ends
with, as the PSP replayer does. Other callers are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Older recorders reused data at any offset in the dump, and Prince of Persia: Rival Swords
(11587, 12943) has a CLUT stored one byte into the previous one. The GE ignores the low four
bits of a CLUT or texture address, so the LOADCLUT read 15 bytes early and the palette
channel-swap pass came out red. Map CLUTs, textures and transfer sources that aren't 16-byte
aligned to a separate aligned copy.
Also updates LocoRoco 12058's software reference, which now matches the PSP's replay exactly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Before 71210f3fa2, a second dump request during a recording started it again, writing another
INIT (and initial CLUT) into the dump: the GE state of that moment, which already includes the
register writes the dump only has after it. Playback applied it, so the last draws ran with the
wrong state: GOD EATER BURST (13950) and GOD EATER 2 (6343) came out black and Street Riders
(14746) garbled. The recorder only writes an INIT when it starts, so skip any later one. The 15
such dumps in the GitHub issue set now match the PSP's replay, 10 of them exactly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The recorder saves the CLUT the GE had loaded right after INIT, as a CLUT command with no
LOADCLUT to follow, unlike the CLUTs it records at a LOADCLUT. Playback only pointed the CLUT
address at it, so draws before the game's first LOADCLUT used whatever CLUT was loaded before:
HotBrain (16131, ULUS10268) drew its save screen background black. Load it, and put the game's
CLUT address back.
A dump whose last DISPLAY turns the display off (address 0), or has none, now shows the last
framebuffer drawn to (Auditorium 9213). The PSP replayer does both the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dump records a texture at a swizzled VRAM mirror as the game sampled
it, and the PSP's replayer writes it back with the CPU, which the PSP
sends through the depth layout at any access width. PPSSPP's playback
wrote it raw, so with depth stored in the GE's layout the texture came
back permuted: IL-2's depth texture at 0x04710000 and the two Gensou
Suikoden swizzle dumps. Texture copies, memcpys and memsets into a
mirror now go through the layout, 32 aligned bytes at a time. Against
PSP replays: both Suikoden dumps are now exact (from 1912 and 8083
pixels off), Star Wars #15832 is 67 off (from 2590) and IL-2 894 (from
70505, the rest from a write the next draw reads before it lands).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the software renderer, which now stores depth as the GE does, a
16-bit read or write at VRAM's 0x04200000 or 0x04600000 mirror goes to
where the GE's 16-bit or 32-bit color layout puts that depth pixel
(ppsspp-re geprobe exp88: the mirrors are the layout's exact inverse).
Games access depth there with nothing else, so other widths stay plain.
The accessors check the address, then whether the GPU core is the
software renderer (CoreParameter); the hardware renderers keep depth plain
and handle the layout their own way. The renderer keeps the layouts up to
date with the EDRAM address translation. With it, the JITs send 16-bit
loads and stores to the interpreter, and the IR passes don't change a
load's width to or from 16 bits.
The pspautotests that read depth through the mirrors now do so 16 bits
at a time, re-recorded on hardware unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The software renderer kept depth linear in VRAM, where the GE stores it
in the layout GPU/Common/DepthSwizzle.h describes, from the EDRAM address
translation and the color format. So a texture or anything else sharing
VRAM with a depth buffer saw depth land in the wrong places (the player's
shadow in Silent Hill: Origins, #22415). Now every depth access goes
through the layout: the generic pixel functions, the 2x2 depth test, the
depth clear in 16-pixel runs, and the x86 pixel JIT, whose IDs carry the
translation so the layout's constants are compiled in. It changes with
the color format and sceGeEdramSetAddrTranslation, after a flush.
Reading VRAM through its 0x04200000 and 0x04600000 mirrors undoes the
layout, as the GE does: texturing from them (drawn from a linear copy
made after a flush), and block transfers from or to them (Hayate no
Gotoku restores its depth buffer through 0x04600000, #17878). Saved and
debugger depth buffers read through the layout, so they're linear.
Socom's lens flare dump now matches a PSP replay where it changes. About
2-3% more cycles in God of War, Tekken 6, Wipeout and Burnout.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Where the GE stores each depth pixel, from the EDRAM address translation
and the color format: with 32-bit color, address bits 5 up to log2(T) - 1
rotate left by one, then (T << 3) | 0x40 is XORed in (0x600, and bits 9
and 10 swapped, at T 0). The CPU sees the same mapping through VRAM's
0x04200000 and 0x04600000 mirrors. Measured in ppsspp-re geprobe exp8-10
(every pixel of 512x272 buffers at each T) and exp88; the unit test checks
it against the model fitted there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tessellator's output was queued as ordinary vertices and run through
a vertex decoder at flush, although the decoded layout is the same as
SimpleVertex. That decode did two things: the UV scale/offset, and
clearing gstate_c.vertexFullAlpha. The tessellator now does both, and
SubmitCurve writes its output where DecodeVerts would have put it and
queues it as predecoded. DecodeVerts only copies such a draw, when it
decodes elsewhere, like the Vulkan and GLES push buffers.
The software renderer decodes by itself and keeps the old path, as do
through mode curves and anything with draws already queued.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MulAdd is fused on ARM64 and LoongArch, which always have it. Rounding to
nearest is a single instruction on every ISA except ARM32.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each row of vertices is evaluated by folding the patch's four control point
rows into four columns with the v weights, then weighting those along u,
four vertices per register with one attribute component each. Cross
products, normalization and the pole test become plain per-lane math, and
the result is transposed into SimpleVertex order on the way out.
The weights are stored one row per term so four steps load as one vector,
and the control points are padded to full vectors in a single array.
Colors now round to nearest on every platform. The SSE path already did,
NEON and the scalar path truncated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>