Commit Graph
48422 Commits
Author SHA1 Message Date
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ård c7a2cb6f63 Merge pull request #22458 from hrydgard/android-small-fixes
Android: Small fixes from a Claude code review
2026-10-08 21:17:00 +02:00
Henrik Rydgård f0e0368a77 Merge pull request #22456 from hrydgard/spline-simd
SIMD-optimize splines and beziers
2026-10-08 20:27:34 +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 a413b88cab Android: Make renderer_inited atomic
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>
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 ce19265949 Don't save log levels set from the command line
--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>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 ea9be99149 Android: Parse a launch parameter sent to the running app as a command line
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>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 105bf522d7 Android: Declare the main activity resizeable explicitly
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>
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ård 0e3dfcef9d Merge pull request #22454 from hrydgard/android-activity-lifecycle
More Android activity lifecycle fixes
2026-10-08 20:10:14 +02:00
Henrik Rydgård 4fa278afe3 Merge pull request #22455 from hrydgard/fix-createdir-chmod
Don't chmod existing parent directories in CreateFullPath
2026-10-08 19:40:29 +02:00
Henrik RydgårdandClaude Opus 5.5 ce97cb1201 Splines: Vectorize the GE-exact spline evaluation
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>
2026-10-08 19:33:20 +02:00
Henrik RydgårdandClaude Opus 5.5 08c1a6907a Don't chmod existing parent directories in CreateFullPath
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>
2026-10-08 19:13:18 +02:00
Henrik Rydgård f87e13feec Merge pull request #22435 from hrydgard/softgpu-depth-swizzle
SoftGPU: Implement depth swizzle (address translation), also for CPU access
2026-10-08 19:08:35 +02:00
Henrik RydgårdandClaude Opus 5.5 2db4096cec Splines: Vectorize the GE-exact Bezier evaluation
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>
2026-10-08 19:00:19 +02:00
Henrik RydgårdandClaude Opus 5.5 802b52d875 CrossSIMD: Add an arithmetic Vec4S32::Shr and Vec4S32FromBits
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:00:19 +02:00
Henrik RydgårdandClaude Opus 5 0c7990783d Docs: How to test an Android build on a real device
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
2026-10-08 18:54:46 +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 Fable 5.1 8b829b3097 Android: Share one TextRenderer class ref between TextDrawers
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>
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Fable 5.1 2f6bb8d55e Android: Don't leak the activity on recreate, and hand it over safely
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
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Fable 5.1 35c586b27b Android: Stop leaking JNI local references on native threads
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>
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Opus 5.5 50ba8e459b SoftGPU: Keep a not-black color test when dithering, fog or a logic op is on
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 75e79ef2db GE dump playback: Let the GE finish the list before reusing its buffer
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 a26ef51a0c GE dump playback: Restore old-layout contexts from old dumps
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 937053063c GE dump playback: Give unaligned CLUTs and textures an aligned copy
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 eb59b00a37 GE dump playback: Ignore a second INIT in a dump
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 a418514376 GE dump playback: Load the initial CLUT, and show the last framebuffer when the display is off
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>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 13c1b4407d frametests: Update IL-2's software reference
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:37:32 +02:00
Henrik RydgårdandClaude Opus 5.5 2317ecbfbd GE dump playback: Write recorded memory into the depth mirrors as the PSP does
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>
2026-10-08 18:37:30 +02:00
Henrik RydgårdandClaude Opus 5.5 ae32477764 Core: Let 16-bit CPU accesses through the depth mirrors see the GE's layout
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>
2026-10-08 18:37:23 +02:00
Henrik RydgårdandClaude Opus 5.5 a11fd22553 SoftGPU: Store depth in VRAM as the GE does
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>
2026-10-08 18:35:48 +02:00
Henrik RydgårdandClaude Opus 5.5 84582c10f1 GPU: Add the GE's depth buffer layout in VRAM
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>
2026-10-08 18:35:48 +02:00
Henrik RydgårdandClaude Opus 5.5 e94d6f9bd2 Splines: Skip the second vertex decode of tessellated curves
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>
2026-10-08 18:12:17 +02:00
Henrik RydgårdandClaude Opus 5.5 f4cf9ab3fd Splines: Use MulAdd, and round colors with one instruction
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:12:10 +02:00
Henrik RydgårdandClaude Opus 5.5 84b674f4d8 CrossSIMD: Add MulAdd and Vec4S32FromF32Round
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>
2026-10-08 18:12:10 +02:00
Henrik RydgårdandClaude Opus 5.5 0b0a30217b Splines: Tessellate four vertices at a time with CrossSIMD
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>
2026-10-08 18:12:10 +02:00