453 Commits
Author SHA1 Message Date
jpolo1224 82f21b16d2 VK: credit sashkinbro for the pipeline cache format and the Adreno split
Both landed as our own commits and both owe him more than they said.

The on-disk pipeline cache header -- length, version, vendorID, deviceID and
pipelineCacheUUID -- is his design from EmuCoreC 47220b153, used as-is, including
using the UUID as the invalidation key so a driver swap rebuilds rather than
feeding a driver a blob it cannot read. What was missing there was the wiring:
nothing passed the cache to vkCreate*Pipelines, so it saved an empty file. That
part, and sharing it with the shader interpreter, are ours.

Splitting Adreno and Turnip out to a 64-wide group size is his too (b9f0f3631).
Our comment had held all of mobile at 32 on the claim that Mali was also 64-wide,
which is wrong -- Valhall warps are 16 lanes and Bifrost 4-8. His split was the
better call and the reason ours changed.
2026-08-20 18:13:35 -04:00
jpolo1224 2ed8442c11 Release: 0.9.4 (versionCode 20) 2026-08-20 17:58:45 -04:00
jpolo1224 ab58fb087b lv2: stop logging sys_memory_container_get_size on every call
Tales of Xillia 2 (BLUS31397) polls this syscall in a loop: 20,664 calls in a 19
minute session, 5,123 of them inside a single second. It was logged at warning, so
each one wrote a line to external storage on Android -- the same shape as the SPU
recompiler diagnostics, and enough on its own to stop frames landing.

Reported as issue #76, an intermittent hang every few minutes that ends with
"Game has stopped responding - it is no longer drawing frames". That message comes
from the 30 second frame-stall watchdog, which is what a saturated log writer
looks like from the RSX thread.

Moved to trace. Nothing is lost: a container's size is fixed at creation, and
sys_memory_container_create and _destroy both still log at warning, so the events
that carry information are still recorded. sys_memory_get_user_memory_size in the
same file already has the equivalent treatment upstream -- it only logs when the
values it reports actually change.

Not confirmed as the cause of #76 yet; the reporter has not attached a log, and 19
minutes of play here did not reproduce the hang. It is the largest log source this
title produces by a wide margin.
2026-08-20 17:55:36 -04:00
jpolo1224 9829bc5402 Android: wait for the previous VM to actually stop before booting
Booting a game while another was still tearing down left the app frozen on the
last frame of the previous game.

The boot gate asked Emu.IsStopped(), whose default overload is

    m_state <= system_state::stopping

so it answers true while the previous VM is still stopping -- and while it is
loading. Kill() only signals the threads and hands the joining to a detached
"Emulation Join Thread", so the state reaches stopping at once and the guard read
that as stopped. The entire wait block was skipped precisely when it was needed,
which is why no "previous VM still running" line was ever logged.

On device: Stopping emulator at 0:10:14, BootGame at 0:10:17, and at 0:10:31 the
join thread was still waiting on PPU[0x1000004] "SPU Interrupt Thread0" -- 17.4
seconds -- with seven SPUs parked in EXIT|w|G-PAUSE and one PPU thread spinning at
100%.

Use the IsStopped(true) overload, which requires system_state::stopped and is
reached only once that join thread has finished. The ten second bound and the
boot-anyway fallback are unchanged, so a teardown that genuinely hangs still gets
reported rather than freezing the UI.

Only the three checks in _rpcsx_boot are changed. Other IsStopped() callers here
want "not running" and the loose overload is right for them.

This does not fix why that interrupt thread fails to exit, which is still open. It
stops a slow teardown from becoming a boot into a half-destroyed VM.
2026-08-20 17:34:07 -04:00
jpolo1224 0fb1757a84 PPU: report thread perf stats once, not on every stop-path pass
cpu_on_stop() is a teardown hook and nothing enforces that it runs a single time.
A PPU thread that re-enters the stop path without exiting reports again on every
pass: one thread ("SPU Interrupt Thread2") was producing "PPU thread perf stats
are not available." roughly every 10 microseconds, near 100,000 lines a second.

That is survivable on a desktop. On Android the log goes to external storage, so
it pins the log writer and drags down the shutdown it is describing -- the
emulator logged "Stopping emulator..." and never reached "All threads have been
stopped", leaving the next boot stuck on the last frame of the previous game.

Guard the reporting with a flag so it happens once per thread. The flag is
deliberately left out of serialization: it describes this run's reporting, not
guest state.

This does not address why the thread re-enters the stop path, which is a separate
question -- it stops that from being an emulator-wide stall while it is open.
2026-08-20 17:19:03 -04:00
jpolo1224 53e225cf96 RPCN: stop burning a core whenever a game is not running
The client thread's inner loop breaks out to the outer sem_rpcn.acquire() for
every state except one: connected and authentified with no game running. The
only blocking wait sits inside the `authentified && !Emu.IsStopped()` branch, so
that case fell through to `while (true)` with nothing to wait on and span a full
core for as long as the user stayed signed in outside a game -- at the menu,
between titles, and throughout shutdown.

Measured on a Retroid Pocket 6 sitting at the library: `RPCN Client` at 100%,
utime 21964 against stime 58, so a userspace spin rather than a syscall storm.

Wait in the fall-through case instead. Breaking out to the outer semaphore would
also stop the spin but nothing releases it when a game starts, so the pings would
never resume.

Only reachable once RPCN actually authenticates, which is why it survived
upstream: signing in is new on Android.
2026-08-20 17:19:03 -04:00
jpolo1224 d9a0481dcb SPU: move the per-block recompiler diagnostics to trace
A 15 minute Prototype session wrote 56,881 log lines, and 49,644 of them came from
the SPU recompiler -- peaking at 2,473 lines in a single second, written to
/sdcard. The bursts land exactly when a game is already stalling to compile new
blocks, which is the worst possible moment to add synchronous file writes, and
they stop when compilation finishes. That matches the reported symptom: seconds of
lockup that recover on their own.

Every one of these is per-block or per-instruction:

    8963  New SPU block compiled successfully   was success
    7833  Precompiling fallthrough              was notice
    4337  Precompiling filler space             was notice
    3449  SPU block is a loop                   was notice
    2418  MFC_EAH / MFC_Cmd not constant        was warning, per INSTRUCTION
    1516  Trampoline simplified                 was error, and is routine
     843  SPU Block Dump                        was notice, and is multi-line
     696  GETLLAR pattern entry point           was notice
    ~1400 PUTLLC16 / pattern breakage family    was notice and success

Upstream can afford these: a desktop has a fast disk and nobody is writing to
external storage. Demoted to trace, so they stay available by raising the SPU
channel and cost nothing during normal play.

The genuine faults keep their level -- MFC_Cmd invalid size and unknown command
are still errors, and they are rare.

This does not claim compilation is free. It removes the logging so what remains
can be measured, which is not possible while the instrument is this loud.
2026-08-20 17:03:56 -04:00
jpolo1224 a2f0059551 VK: run the conversion kernels 64-wide on Adreno
Adreno waves are 64 lanes, and a workgroup narrower than the wave does not pack
together with its neighbours -- it occupies a whole wave and masks the surplus
lanes off. At 32 that idled half of every wave on every dispatch, and these
kernels run on every texture upload of a kilobyte or more, plus every deswizzle
and detile.

Nothing argues the other way here. The kernels carry no shared memory and no
barriers, so group size is a scheduling hint with no LDS or synchronisation
cost to trade against.

Adreno and Turnip only. The comment this replaces claimed Mali was also 64-wide
and used that to justify holding everything at 32; that is wrong -- Valhall
warps are 16 lanes and Bifrost 4-8, so 32 already spans several of them and
there is no half-empty wave to reclaim. Xclipse is RDNA-derived and prefers
wave32 for compute. Both stay where they were.

Group size is baked into the generated GLSL, so this invalidates shader and
pipeline caches once on first launch after the update.
2026-08-20 16:22:21 -04:00
jpolo1224 cbee3cd44b VK: allow the compute work group size to be overridden for benchmarking
The mobile branch of the per-vendor group size table picks 32 by falling
through to the NVIDIA case. Adreno and Mali both run 64-wide waves, so 32
plausibly leaves half of each wave idle -- but that is reasoning, not a
measurement, and guessing wrong here costs performance silently.

Read ARMSX3_CS_GROUP_SIZE, which driver_env.txt already plumbs through
setenv(), so both candidates can be compared on one build without a settings
field or a second APK. Powers of two only, clamped to maxComputeWorkGroupSize
and maxComputeWorkGroupInvocations, because an over-large local_size_x fails
shader compilation rather than validation. The default is unchanged; this only
makes the question answerable.
2026-08-20 16:08:20 -04:00
jpolo1224 5d71742da9 VK: persist the driver pipeline cache across runs
Every vkCreate*Pipelines call passed VK_NULL_HANDLE for the pipeline cache, so
the driver re-did the whole of its own compilation work for every pipeline, in
every run. On mobile that work is a visible stall the first time each pipeline
is seen -- and it was being thrown away at every shutdown.

Give render_device a VkPipelineCache seeded from <cache>/vk_pipeline_cache.bin
and written back at teardown, and hand it to both pipeline creation calls. The
file is keyed on vendorID, deviceID and pipelineCacheUUID, so a driver update
or an adrenotools driver swap rejects the old blob and rebuilds rather than
feeding a driver a cache it cannot read. Oversized blobs are dropped instead of
being allowed to grow without bound -- the cache is shared by every title, so
one cold run is the cheaper failure.

This is orthogonal to the RSX shader cache: that one remembers WHICH pipelines
a title needs, this one makes each one cheap to create.

The shader interpreter was opening a private cache and destroying it on the way
out, which discarded exactly the expensive ubershader compiles. It now borrows
the device's. Teardown order already guarantees the pipe compiler threads are
joined before the device is destroyed, so the readback needs no extra locking.

vkGetPipelineCacheData was missing from the generated Android dispatch table;
regenerated with it, no other entry point changed.
2026-08-20 16:08:07 -04:00
jpolo1224 21a64b9eb7 Merge RPCS3 upstream: ROP output remap and an ISO magic-check fix
Seventeen commits. The substantial one is kd-11's ROP_OUTPUT_REMAP series
across rsx/fp, glsl and both backends, which ARMSX3 did not have at all.

Two conflicts.

nv4097.cpp: upstream added the ROP remap plumbing to the format-change checks,
we have profiler instrumentation and an ARM64 observe() on the two hot FIFO
reads. Different parts of the same file, so upstream's version is the base and
ours is re-applied on top. The g_xform_const_words increment is included
deliberately: the profiler reports average batch size as words/calls, so
dropping it would have printed 0 rather than nothing, which is worse than an
absent stat.

ISO.cpp: took upstream's magic-read check. It is a real fix --
`!file.read_at(...) == 5` parses as `(!x) == 5`, which is false for every x, so
the guard never fired and a short read left `magic` uninitialised. Our reverted
reader has no check there at all, and read_at returns a byte count in this
version too, so the corrected form applies cleanly.

This does NOT undo the ISO reader revert. The refactor that broke reading for
some users is still reverted; only the one-line magic check comes across.
2026-08-20 15:42:37 -04:00
jpolo1224 39571dc7c9 RPCN: stop sending requests with required fields empty
"Server error 1" is ErrorType::Malformed, and it was our fault rather than the
server's: two buttons posted queries with a required field blank, and the
server rejects the whole query rather than naming the field.

Reset password sends the account's email so the server can mail a token, but
the email box was only rendered while creating an account. Outside that mode it
sent an empty address every time. The box is always shown now.

Resend token sends the password, and Save deliberately clears that box, so
anyone who saved before pressing it sent an empty one. Having typed it a moment
ago is not the same as it being in the field.

Both are checked before sending now, along with account creation, and the
message names the field to fill in rather than failing at the server.

The error text is better too. Malformed, Invalid and the unknown case were
surfacing as a number with no way to act on it. Malformed now says outright
that it is a bug and asks which button was pressed, because if it appears again
the guards above have missed a path.

Not fixed here: sign-in reporting Invalid Password for credentials that work on
desktop. That is a distinct server code rather than a catch-all, and the cause
is not yet known -- desktop stores the password exactly as this does, so it is
not a hashing difference.
2026-08-20 14:06:40 -04:00
jpolo1224 bfeb232b28 Release: 0.9.3.1, and make upload signing opt-in
The ISO reader revert is confirmed working by the reporter, so this ships it.

Upload signing is now requested explicitly with -Parmsx3.uploadSigning rather
than used whenever keystore.properties happens to exist. Once that file was
created, every release build silently started signing with the upload key, and
an APK signed differently from the one already installed cannot be installed
over it -- so a sideload build becomes something testers cannot install, and
Android's error does not mention signatures.

It was being worked around by hiding keystore.properties by hand before each
build, which is the kind of step that gets forgotten exactly once and then
wastes a tester's evening. build-play-aab.sh passes the flag; nothing else
does, and asking for it without the keystore present is now an error rather
than a silent fallback.
2026-08-20 13:16:42 -04:00
jpolo1224 f707458b07 Revert the upstream ISO reader refactor
The upstream merge brought a six-commit ISO series that regressed reading on
some images. A tester's ISOs that loaded on 0.9.2 now fail at the very first
header read:

    ISO: init: Failed to read region information (region_count=0)
    ISO: iso_archive: Corrupt ISO file: Decryption failed

Both of his games fail identically, so it is not one bad dump, and other
people's ISOs still load, so it depends on the image format -- most likely the
3k3y path, which has its own watermark detection and key extraction at 0xF70
and 0xF80, separate from the plain one.

The reader is reverted to its 0.9.2 state. The change implicated is the region
count read moving from char_arr_BE_to_uint(sec0_sec1.data()) to
read_from_ptr<be_t<u32>>(sec0_sec1), alongside decrypt_data moving from raw
pointers to std::span, but this is not a targeted fix: it restores code that
demonstrably worked rather than guessing which line of the refactor is wrong.

is_valid() is kept, because System.cpp calls it and it is one line that has
nothing to do with the failure.

What this gives up is real: the series contained out-of-bounds write and
overflow hardening for ISO parsing. A reader that cannot open a user's games
is the worse of the two, and the hardening should come back with a version of
the refactor that works. The path validation from the same series that lives
outside these two files is untouched.

Unverified against the actual failure -- the ISOs here all load either way, so
this could not be reproduced locally. It is verified not to break what already
worked.
2026-08-20 12:48:38 -04:00
jpolo1224 eaba425972 Build: stamp the real commit into the version string
Builds were reporting whichever commit cmake last configured against, not the
one being built. rpcs3/git-version.h is generated at CONFIGURE time, and
build-variants.sh deliberately skips reconfiguring an already-correct build
dir because re-running cmake regenerates LLVM's headers and costs a full
rebuild. So the stamp froze, and every build after that lied about itself.

The gap was wide. HEAD is 20120-da26a455; the header on disk still said
19985-91952ae4, a commit from before 0.9.2.

This is not cosmetic. A tester running 0.9.3 reported that older commit in
their log, which sent an investigation hunting for a regression among upstream
ISO changes their build did not contain, and very nearly had a fix built and
sent for a version they were not running. A build that misreports itself makes
every report from it ambiguous.

stamp-git-version.sh writes the header from HEAD and is called by both build
entry points before anything compiles. It only writes when the contents differ,
so an unchanged HEAD does not force a rebuild, and ninja rebuilds just the
translation units that include it.
2026-08-20 12:43:45 -04:00
jpolo1224 da26a45548 Build: stage the legacy core in the Play script rather than trusting jniLibs
The script bundled whatever core happened to be sitting in jniLibs, and
build-variants.sh overwrites that path once per variant, so the file left there
is simply whichever ran last. Running the variants and then rebuilding the
bundle would have shipped an armv8.2 core inside a minSdk 30 bundle:
installable on devices that cannot execute it, failing at dlopen, with nothing
in the failure to point at the cause.

Play serves one bundle to every device, so the ISA floor has to be the lowest
ARMSX3 supports. The legacy core is now stripped into place by this script
every time, and it refuses to run if that core has not been built.

The bundle already shipped was correct -- the legacy core was staged by hand
before it was built -- but only by hand, which is the part worth removing.
2026-08-20 12:18:23 -04:00
jpolo1224 05e4547f67 Build: a grep -q behind pipefail inverts every positive check
The bundle verifier reported libarmsx3-core.so missing from a bundle that
plainly contained it, at base/lib/arm64-v8a/libarmsx3-core.so.

Under `set -o pipefail`, `unzip -l "$AAB" | grep -q x` fails whenever x IS
present: grep exits at the first match, closes the pipe, and unzip takes
SIGPIPE, so the pipeline reports failure. Every check for something that must
be there was inverted, and every check for something that must be absent passed
for the wrong reason -- grep found nothing, read to the end, and no signal was
raised.

Capturing the listing first and piping printf into grep instead only moved
which process took the signal. The listing is matched with `case` now, which
has no subprocess to kill.

Worth stating plainly: this was a verifier that would have reported a clean
bundle whatever went wrong, which is the failure mode the script exists to
prevent. It only surfaced because the one check that should have passed was the
one that failed.
2026-08-20 12:00:21 -04:00
jpolo1224 d02079f7c8 Build: reject a half-configured upload key, and find a JDK
Checking that keystore.properties exists was not enough. A template with the
placeholders still in it passed the check and went on to build, failing much
later inside signing with an error that says nothing about the cause. It now
rejects the placeholders too, so the message arrives before the fifteen minutes
rather than after them.

Also locate a JDK. The script assumed the calling shell had one on PATH, and
without it gradle fails with "Unable to locate a Java Runtime" several steps in,
which points at Java rather than at the environment. Android Studio's bundled
JDK is used when JAVA_HOME is unset, with /usr/libexec/java_home as a fallback
and a clear failure if neither exists.
2026-08-20 11:52:37 -04:00
jpolo1224 737a6927f1 Build: upload signing for Play, and minify off where it has to be
Two things stood between the flavor split and an uploadable bundle.

The release build type was debug-signed, with a comment saying to swap it
before any public build. It now uses an "upload" signing config when
keystore.properties exists and falls back to the debug key when it does not, so
GitHub alphas stay sideloadable without the key present while Play gets a
properly signed bundle. The keystore and the properties file are both already
gitignored, and nothing reads or echoes them outside the build.

build-play-aab.sh refuses to start without that file and prints the keytool
command to create one, rather than spending fifteen minutes producing a bundle
Play will reject.

Minify is off for the Play bundle and on everywhere else. This is not a
preference: AGP 9.2.1's R8 writes its mapping as mapping.prt, a compressed
per-class archive, while packageBundle still requires a plain mapping.txt, so
an AAB cannot be built with R8 enabled at all. ARMSX2 ships its Play build with
minify off entirely, so this matches existing practice rather than introducing
a compromise, and a 94 MB native core dominates a 76 MB APK -- shrinking the
Kotlin was never where the size is.

Driven by a gradle property, matching how armsx3.minSdk is already threaded
through by build-variants.sh, rather than by the variant API.

Verified: the github release APK still builds at 76.5 MB with R8 running.
2026-08-20 11:50:27 -04:00
jpolo1224 0b4d9599e8 Build: verify the Play bundle by extracting it, not by grepping it
An AAB is a ZIP and its entries are compressed, so grepping the archive finds
nothing and reports a clean bundle whatever is inside it. The first version of
this script did exactly that. It passed all five exclusion checks and then
failed to find the applicationId it was supposed to find, which is the only
reason the method got questioned at all -- every check that mattered was a
false pass.

It extracts the bundle now and reads base/manifest/AndroidManifest.xml for the
permissions, and the archive listing for the native libraries. Verified against
a real bundle: com.armsx3.play present, REQUEST_INSTALL_PACKAGES,
MANAGE_EXTERNAL_STORAGE, RECORD_AUDIO and the update provider all absent, and
libarmsx3_lsfg.so not among the shipped .so files.

The manifest is protobuf rather than text, so LC_ALL=C and grep -a stay: the
NUL bytes in it would otherwise make grep declare the file binary and print
nothing, which reads the same as a pass.
2026-08-20 11:46:58 -04:00
jpolo1224 769b64cb3a Build: split github and play flavors for a Play release
Play will not take the build ARMSX3 ships today, and the two runtime flags that
were already here could never have made it acceptable. The manifest said so:
it is the declared PERMISSION in the bundle that gets rejected, not the code
path behind a boolean. So the split is by source set, which is the only thing
that actually removes anything.

github keeps what a sideloaded build should have: the in-app updater, all-files
storage for an arbitrary data folder, and frame generation. play has none of
them, and its applicationId is com.armsx3.play so the two install side by side
instead of over each other.

Moved rather than flagged: com.armsx2.update and its FileProvider resource into
src/github, with no-op composables in src/play so that source set still
compiles. REQUEST_INSTALL_PACKAGES and MANAGE_EXTERNAL_STORAGE now live in the
github manifest, so the play bundle never declares them.

Frame generation is excluded by moving libarmsx3_lsfg.so into
src/github/jniLibs. A packaging block inside a productFlavor is NOT honoured --
it applied to both and dropped the library from the github build too, which the
build caught. Excluding the file is the whole exclusion: the shim is dlopen'd
by name and the core already reports the feature unavailable when it is absent,
which is the same path a device that cannot run it takes.

build-play-aab.sh builds the bundle and refuses to emit one that fails any of
five checks, because a flavor split is only worth as much as the thing that
notices when it stops working. It greps the bundle with LC_ALL=C and -a on
purpose: a NUL byte makes plain grep declare the file binary and say nothing,
which reads exactly like a pass.

build-variants.sh follows the rename to assembleGithubRelease; there is no
flavorless release variant any more.
2026-08-20 11:18:53 -04:00
jpolo1224 4b147a71af Save data: an unfinishable save fixup is not fatal
Both failures in the savedata fixup were logged as fatal while the code
carried straight on -- the loop continues, the emulator starts, and one save
is left as it was. On Android a fatal line goes to logcat at ANDROID_LOG_FATAL,
so it reads like a crash, and it was reported as one.

It is also permanent rather than transient when it happens. Android storage can
refuse a directory rename outright with EPERM, and nothing about launching
again changes that, so it fired on every single launch and looked like a fault
that was getting worse. A user hitting it has no way to tell that their games
are unaffected.

Both are errors now, and both say what the consequence is and what to delete if
it keeps happening, rather than repeating an alarm about something the emulator
has already decided to continue past.
2026-08-20 11:07:20 -04:00
jpolo1224 a7093fe962 Release: 0.9.3 (versionCode 18) 2026-08-20 08:35:23 -04:00
Megamouse bab81aa23e Fix rpcn type cast warnings 2026-08-20 11:03:16 +02:00
Megamouse 8a6c96745a Fix iso magic read check 2026-08-20 11:03:16 +02:00
Megamouse 358101c47b Fix unused variable warnings 2026-08-20 11:03:16 +02:00
yahfz c4eff69711 SPU LLVM: Document AVX-512 XFloat lowering 2026-08-20 09:36:19 +03:00
yahfz 5d2601599e [SPU LLVM] Optimize AVX-512 XFloat conversion 2026-08-20 09:36:19 +03:00
jpolo1224 07a24cf71d PPU: stop on a compile out-of-memory rather than limping on
Continuing with the modules that did compile is correct -- the dispatcher
entry for an uncompiled function interprets, nothing runs garbage -- but it is
per-instruction dispatch and it is slower than the interpreter outright. Saint
Seiya measured 6fps against 23 with most of its modules missing.

A game at that speed looks broken, and it gets reported as broken, when the
real answer is one restart away. An honest stop is better than a degraded run
that invites the wrong bug report.

So this ends the boot the way running out of memory always did. What is
different from before is the reason: the message names it and says what to do,
and it stays on screen because the overlay is drawn by the RSX thread rather
than the one this stops.

The fallback itself stays in place for the ordinary case of a single module
failing for some other reason, which is upstream's design and is worth keeping
-- one bad module costing its own functions is a fair trade. Running out of
memory is not that case: it takes most of the executable with it.
2026-08-20 00:33:33 -04:00
jpolo1224 187654eae6 Revert the frame-generation work entirely
Both changes that were brought back are out again. Neither recovered the frame
rate after switching frame generation off, and both broke rendering.

That is every attempt at this reverted. What remains is the code as it stood
before any of it: frame generation is slow, and switching it off does not
release what it allocated.

The GLES DSA shim is kept, because it belongs to the upstream merge and not to
this work -- it landed in the wrong commit and reverting that commit took it
with it, which broke the build for a reason unrelated to frame generation.

The findings from the three audits are still correct as descriptions of what
the code does. Acting on them, one at a time and with the reasoning written
down each time, still made the result worse on every attempt. So the fault is
not in the individual fixes but in something about this subsystem that reading
it has not revealed, and the next attempt should start from a measurement on
the device rather than from another reading of the source.
2026-08-20 00:16:33 -04:00
jpolo1224 276404e19d Framegen: restore the sync-fd handoff
This is the change that stopped turning frame generation off leaving the
frame rate tanked -- confirmed by which build that was reported on, and by the
SUBOPTIMAL acquire fix not reproducing it on its own.

It replaces the per-frame vkDeviceWaitIdle on framegen's device with a poll on
a sync fd exported from a fence framegen already creates. Reasons it is safe
to bring back while the other three stay reverted: it only runs when frame
generation is producing frames, every failure path falls back to the device
wait, and it changes no layout, no ownership and no barrier -- which is where
the rendering damage came from.

Still out, and staying out: the cross-device ownership barrier, the
generated-frame acquire semaphore with its teardown, and the deferred-present
guard in frame_context_cleanup.
2026-08-20 00:11:43 -04:00
jpolo1224 380f18484d VK: restore the SUBOPTIMAL acquire fix
Swept up in last night's blanket revert of the frame-generation work, and it
should not have been. It is the one change of the five that cannot affect
rendering: it corrects error handling and touches nothing about layouts,
ownership or synchronisation.

VK_SUBOPTIMAL_KHR is a success code and the image IS acquired.
present_generated_frame treated it as a failure and returned without
presenting, so the image was never handed back -- one swapchain image lost per
generated frame, permanently, on a driver that reports SUBOPTIMAL as a
standing condition. Once the pool empties the real frame's acquire blocks its
whole 100ms timeout every frame, which is ten frames a second, and only a
swapchain rebuild recovers it. That is why turning frame generation off never
gave the frame rate back.

The generated present's result is handled for the same reason: OUT_OF_DATE and
SURFACE_LOST also leave the image unpresented, and none of the recovery flags
were being raised because generated frames never pass through present().

The other four stay reverted. This one is restored on its own so that if
anything is still wrong, the cause is a single small change rather than five
entangled ones.
2026-08-20 00:07:39 -04:00
jpolo1224 2844667673 Merge RPCS3 upstream, and revert tonight's frame-generation work
Catches up 98 commits from RPCS3/rpcs3. Seven files conflicted:

PPUTranslator.cpp -- upstream fixed the ARM64 float-to-int saturation
inversion independently, and the two fixes are the same fix. Took upstream's
ordering so it stops re-conflicting, kept one line of the reasoning.

rpcn_types.h, rpcn_client.cpp -- our hand-rolled protocol 31 bump was a
stopgap to stop the server refusing us. Upstream's trophy sync (cb175278b) is
the real implementation, so it replaces ours outright.

nv0039.cpp -- upstream refactored the strided copy and extracted
validate_buffer_notify; ours was the older code plus a profiler include. Took
upstream's, re-added the include.

VKQueryPool.cpp -- both sides had real changes. Upstream added a lock around
the pool cache; ours has the bounded occlusion-query wait and the render-pass
fix for the Adreno device loss. Kept ours, re-applied their lock.

VKResourceManager.cpp -- upstream moved GC completion onto a
driver_manager_thread, which supersedes our flush parameter and offloader
dispatch entirely. Took theirs; no callers passed the second argument.

VKGSRender.cpp -- our flush-site counter and their driver-manager drain are
independent. Both kept.

Upstream also began attaching 3D and array levels through DSA, which
EXT_direct_state_access has no NamedFramebufferTextureLayer for, so the GLES
shim gains one that binds and uses the non-DSA entry point.

Reverted in the same commit, because they are what is on the tester's device
and it is broken: the sync-fd handoff, the SUBOPTIMAL acquire fix, the
cross-device ownership barrier, the generated-frame acquire semaphore, and the
deferred-present guard in frame_context_cleanup. Each was defensible on its
own reading of the code and the result was worse every time -- slower, judder
described as nauseating, and rendering faults that outlast switching frame
generation off. Five attempts is enough to stop treating the next theory as
better than the last.

What stays fixed is everything outside the present path. What goes back is the
state before tonight: frame generation is slow and does not release its memory
when switched off.
2026-08-20 00:01:42 -04:00
jpolo1224 da11248a31 VK: synchronise the generated-frame acquire, and free framegen with the renderer
Two things frame generation was getting away with rather than doing.

The acquire for a generated frame passed VK_NULL_HANDLE for both the semaphore
and the fence. The specification forbids that outright, and the practical
consequence is that nothing made the blit wait for the presentation engine to
finish with the image -- the UNDEFINED old layout on the barrier discards
contents, it does not order anything, so the blit could overwrite an image
still being scanned out. It acquires with a semaphore now and the submit waits
on it.

A ring of eight rather than a single semaphore, because a binary semaphore may
not be waited again before it has been signalled again and up to three
generated frames are acquired per flip. Eight is more than two flips of slack,
so a slot is never revisited before the submit that waits on it has retired.

The other is a crash rather than a hazard. Frame generation's images are file
-scope globals created on the renderer's device, and nothing released them, so
stopping a game and starting another at the same resolution skipped the
rebuild -- capture_presented_frame only rebuilds when the dimensions change --
and recorded images belonging to a destroyed device into a live command
buffer. They are released in ~VKGSRender now, after the vkDeviceWaitIdle it
already does and before the swapchain goes.

That location is the point. An earlier attempt at the same teardown ran from
inside the capture hook, which executes while the frame's primary command
buffer is still being built, and it destroyed rendering outright. A
device-idle point the renderer owns is where this always belonged.
2026-08-19 23:49:17 -04:00
jpolo1224 b3b8e707bc VK: acquire framegen's output before reading it
The blit that puts a generated frame on screen named
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL for an image that was never in that
layout, and read it without ever taking ownership from the device that wrote
it. framegen releases its outputs with newLayout = VK_IMAGE_LAYOUT_GENERAL and
dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL, so this was a layout mismatch
and a missing queue-family acquire at the same time.

Undefined by the specification either way, and on a tiler the practical result
is reading whatever survived in cache instead of what framegen wrote -- which
is the corrupted rendering that appears whenever frame generation is switched
on.

The image is acquired from VK_QUEUE_FAMILY_EXTERNAL into the graphics family
and transitioned GENERAL -> TRANSFER_SRC_OPTIMAL before the blit, then
released back the same way afterwards, because framegen's own acquire barrier
on the next generation assumes it finds the image exactly as it left it.

This is about visibility and ownership, not timing: generate() already does
not return until framegen's submission has completed.
2026-08-19 23:46:19 -04:00
jpolo1224 32b3e17fd7 VK: stop generated frames leaking a swapchain image on every SUBOPTIMAL
present_generated_frame treated VK_SUBOPTIMAL_KHR as a failure. It is a
success code and the image IS acquired, so the early return left it held and
never presented -- one swapchain image gone per generated frame, permanently.
This driver reports SUBOPTIMAL as a standing condition rather than a one-off,
which present() already accounts for a few lines below, so the pool emptied
within a few frames of turning frame generation on.

Once it is empty the real frame's acquire in flip() blocks its full 100ms
timeout every frame. 100ms is ten frames a second, which is the hard lock that
was reported, and it explains the part that made no sense: turning frame
generation back off changed nothing, because the images are gone from the
swapchain and only a rebuild -- a resize or a restart -- brings them back.

The present result was discarded for the same frames. OUT_OF_DATE and
SURFACE_LOST also leave the image unpresented, and none of the recovery flags
were raised, because generated frames never pass through present() where that
handling lives. Both are now routed the same way.

Also submit with flush before presenting. The present runs on this thread
while multithreaded RSX would still have the submit queued on the offloader,
which presents a swapchain image before the blit that fills it.
2026-08-19 23:39:11 -04:00
jpolo1224 43dcf6d0ca Framegen: wait on a sync fd instead of idling framegen's whole device
Every generated frame ended with vkDeviceWaitIdle on framegen's device. It is
the hardest sync primitive Vulkan has and it ran once per frame, which is a
large part of why frame generation cost more than it returned.

It was there because the two devices share no semaphore. The upstream
semaphore path cannot supply one on Android: framegen's device deliberately
does not enable VK_KHR_external_semaphore_fd -- it shares through
AHardwareBuffer instead -- so vkImportSemaphoreFdKHR resolves to nullptr there
and every semaphore handed across is -1. That is not a flag that can be
flipped.

A fence can be exported where a semaphore cannot. framegen already creates one
per generation pass, so this adds VK_KHR_external_fence_fd, exports a sync fd
after the passes are submitted, and hands it back through a new
armsx3_lsfg_present_fenced. Our side polls that fd instead of idling a device.

Three things this deliberately does not do:

The existing completion fence is not the one exported. SYNC_FD export has copy
transference, which resets the source fence, so exporting it would leave the
slot-reuse wait at the top of present() blocking on a fence with no pending
signal -- a hang traded for a stall. A dedicated fence signalled by a
zero-batch submit is used instead, which signals once all previously submitted
queue work completes.

The extension is probed, not required. framegen's required-extension list
throws on a miss, so a driver without it would fail vkCreateDevice and lose
frame generation altogether -- a regression rather than a degradation.

The device wait stays as the fallback. A null entry point, a failed export, a
poll timeout or a poll error all fall back to it, and the warning for that is
emitted once per session rather than once per frame.

armsx3_lsfg_present keeps its signature and forwards to the new path with a
null fd, so the two bodies cannot drift. ABI goes to 3; the version check is
left exact rather than loosened.
2026-08-19 23:28:22 -04:00
jpolo1224 cd829558a8 Revert "VK: turn frame generation pipelining back on"
This reverts commit 35b374f068.
2026-08-19 23:20:52 -04:00
jpolo1224 35b374f068 VK: turn frame generation pipelining back on
The serialised path submits the frame, then blocks on that whole submission
before framegen may read the capture, then blocks again on framegen's device.
Two full stalls per frame, one of them a complete frame of GPU work on the
critical path. That is the reason frame generation costs more than it gives
back on this hardware.

Pipelining exists to remove the first of those: it runs a frame behind and
interpolates the pair ending at the PREVIOUS frame, which the game has had a
whole frame to finish, so the wait is normally already satisfied when it is
reached.

It was disabled because the held-back frame could be recycled underneath it.
That is fixed at the choke point in the commit before this one, so this is
the switch and nothing else.

Kept as its own commit deliberately: if the pipelined path misbehaves on
device, reverting this alone returns to the serialised path with the reclaim
fix still in place.
2026-08-19 23:16:26 -04:00
jpolo1224 3d257cfc0f VK: pay the deferred present from frame_context_cleanup, not one call site
Frame generation's pipelined path holds one frame back by a present, and the
guard that stopped that frame being recycled underneath it lived in
advance_queued_frames. There are two functions that walk m_queued_frames and
call frame_context_cleanup, and that was one of them.

check_present_status() is the other. It had no check at all, and
flush_command_queue calls it unconditionally on the way out -- so any of the
twenty or so flushes in a frame could retire the held-back frame, and
flush_command_queue(true) drains the queue outright, which made it certain
rather than likely. flip()'s own acquire-retry spin calls it too, exactly when
swapchain images are scarce, which is the condition pipelining creates by
design. That is the "already retired" that fired twice on device and kept
pipelining switched off.

The guard moves into frame_context_cleanup itself, which is the single point
every reclaim path goes through, so a seventh path added later cannot miss it.

Also stop a dropped present leaving a stale present_image behind. Leaving it
set is not harmless: the slot rotates back around and trips
ensure(present_image == umax) in flip(), and reinitialize_swapchain selects
contexts by that field and then calls frame_context_cleanup, whose first line
asserts a command buffer the context no longer has. Dropping a present should
cost a frame on screen, not a fatal two frames later.

No behaviour change on its own: nothing sets m_deferred_present_frame while
pipelining is disabled.
2026-08-19 23:16:26 -04:00
jpolo1224 4f310af3e2 Revert: do not tear framegen down from the capture hook
Releasing framegen's resources when the setting went to off broke rendering.
Arkham City lost its character models, then almost everything stopped drawing.

Both halves are reverted: the release_shared_images() call and the shutdown()
that followed it. Adding a vkDeviceWaitIdle on our device before the release
did not fix it, which rules out the in-flight capture blit being the whole
story -- the damage reaches past the shared images, and I do not yet know how
far. Guessing again on a renderer that draws nothing is not worth the trade.

What the revert restores is the known-good behaviour and the known bug with
it: switching frame generation off keeps every byte it allocated, so the frame
rate does not recover until the game is restarted. That is worse than it
should be and better than a black screen.

The teardown itself is still the right thing to do. It has to happen somewhere
the present path is not mid-flight -- a device-idle point owned by the
renderer -- rather than from inside the capture hook, which runs while the
frame's primary command buffer is still being built and submitted.
2026-08-19 23:05:10 -04:00
jpolo1224 97d9abd579 VK: keep shader compilation off the cores the frame depends on
Shader compilation "dips" are not the renderer waiting on a pipeline. Both
async shader modes are built so nothing ever stalls for one -- the interpreter
draws it or the draw is dropped. The dip is the compile work itself competing
for cores the emulator is already short of.

The pipeline compiler workers never set an affinity at all. PPU, SPU and RSX
all do, and the big.LITTLE policy deliberately fences the SPUs off the prime
core so RSX can have it -- and then a shader burst was free to land on exactly
that core anyway. They are pinned to thread_class::general now, which is the
existing policy for helper threads and already resolves to the little cluster
on big.LITTLE and to every core on a uniform machine, so desktop is unchanged.

The pool was also sized to the wrong thing. The auto heuristic counts host
threads, so an 8-thread phone asked for 2 workers -- a number chosen for cores
those workers are now not allowed on, while its 3 little cores sat idle
through every burst. It sizes to the cluster it is pinned to instead, which
raises compile throughput without taking anything back from emulation: the
cores it adds are the ones nothing else wanted.

Not touched: the driver's compiler itself, which is Qualcomm's and is the
actual cost. This only stops it fighting the emulator for the same cores.
2026-08-19 22:51:42 -04:00
jpolo1224 8c3a017b51 VK: shut framegen's device down too when it is switched off
Releasing the shared images and the context was not enough. Measured on
device: the release ran exactly once at the moment frame generation was turned
off, and the frame rate still did not come back.

What was left is framegen's own VkDevice. It is created lazily on first use
and nothing ever tore it down -- shutdown() existed with no callers at all --
so a second logical device stayed alive on the same GPU for the rest of the
process, holding queues, allocators and driver-side state. On a tiler that is
not free, and it is the part that survived the previous fix.

Safe to do from here because initialize() is lazy and guarded by its own
initialized() check inside generate(), so switching frame generation back on
rebuilds the device rather than finding it missing.
2026-08-19 22:48:40 -04:00
jpolo1224 7c03c75903 VK: give the memory back when frame generation is switched off
Turning frame generation on cost performance, as it should. Turning it off did
not give any of it back, and only restarting the game did.

Everything frame generation needs is allocated on the present path and nowhere
else: two shared input images, up to three generated outputs, and a context on
framegen's own Vulkan device holding imported AHardwareBuffers for all of
them. The "off" branch of that path was a bare early return, so none of it was
ever released -- the feature stopped doing work while keeping every byte it
had taken. On a tiler that is the expensive half.

The teardown already existed and was reachable from exactly one place, the
resize path, which is why this survived: it needs frame generation to have run
and then be turned off in the same session to show at all.

Only the mode going to off frees anything. The same branch also caught a frame
with no source image, which is a transient state on the present path rather
than the user switching the feature off -- tearing down there would destroy
and rebuild the context repeatedly. The two conditions are separate now.
2026-08-19 22:41:39 -04:00
jpolo1224 ac897144b3 PPU: survive running out of memory while compiling, and say so
Arkham City never finished compiling, and neither did LEGO Batman 2. The log
said "LLVM crash recovery invoked" 240 times and then killed main_thread,
which looks like a codegen bug and is not one.

What actually happened: utils::memory_commit failed with ENOMEM inside the
disposable LLVM worker. That thread dying is how run_recoverable_llvm reports
any failure, so an out-of-memory device was indistinguishable from bad
codegen. It cost a long detour through max_map_count, disk space and
overcommit before the errno in the fatal gave it away, so the JIT's allocator
now uses a checked commit and throws a plain "Out of memory" instead.

The fatal part was the symbol resolvers. ppu_initialize ensure()d that every
group's __resolve_symbols was present, but that function lives in the compiled
output: when every module in a group fails, it is simply absent. The ensure
turned a partial compile into a dead main_thread, which discarded the 170
modules that HAD compiled and surfaced as a boot that never ends.

That contradicts the design either side of it -- a module that fails to load
is deliberately not fatal, because a guest function with no compiled code
keeps its dispatcher entry and is interpreted. A missing resolver is now the
same: report it, skip it, let that group interpret. Losing one group's speed
beats losing the boot.

Also tell the user. Out of memory is the only compile failure they can act on,
and the useful action is not obvious: compiled modules are already in the
cache, so starting the game again resumes rather than restarting the work. One
message after the workers join, not one per module -- once memory is short
every remaining module fails identically, and two hundred popups would be
worse than none.

Lowering Max LLVM Compile Threads also avoids it, and is deliberately not
suggested in the message: compile time is already the common complaint, and
halving the workers to dodge a case that is now survivable is a bad trade.
2026-08-19 22:21:58 -04:00
jpolo1224 d1e1d9975b Input: connect vibration, which was three unconnected halves
Rumble did nothing in any game. Everything needed for it existed and none of
it was wired to anything else.

The core side was fine throughout: the virtual pad is created with
CELL_PAD_CAPABILITY_ACTUATOR, its two VibrateMotors are initialised, and
_rpcsx_getPadRumble reads them, so cellPadSetActDirect had somewhere to land.

Above that, three dead links in a row.

The pump that polls getPadRumble and drives the phone's vibrator was written
and never started -- startRumblePump had no callers at all. The core cannot
notify the JNI layer when the guest writes the motors, so with nothing
polling, no guest rumble could ever reach the device. It now runs for the VM's
lifetime, started once the emulator is past boot and stopped in a finally, so
the abnormal-teardown path that exits via stopRequested cannot leave the motor
buzzing on whatever value a dead guest last wrote.

The enable flag was two flags. NativeApp.sRumbleEnabled had three writers --
the settings toggle, the in-game menu and app start -- and no readers. The
pump consulted a private copy in Rpcs3Bridge that only setPadVibration wrote,
and setPadVibration had no callers either. So the toggle wrote one store and
the pump read another. There is one flag now; the private copy is a read-only
accessor onto it.

Rumble is polled on port 0 only. The device has one vibrator, so there is
nothing to spend a second port on.
2026-08-19 21:25:47 -04:00
jpolo1224 007c4a56a1 RPCN: say on entry that the account is saved
The account screen reported nothing until a button was pressed, so after a
restart a saved account looked like no account: the username was filled in and
the password box said one was stored, but nothing said the thing the user
wanted to know.

It now reads the state from the core on entry and says so.

The wording matters here, because "logged in" is not a state that exists.
RPCN keeps no persistent session -- every connection re-authenticates from the
saved credentials, and the client is destroyed as soon as the last shared_ptr
to it goes, which happens the moment a sign-in test returns. So a restart
cannot preserve a login, and claiming one would be a lie that breaks the first
time someone checks. What survives is the ACCOUNT, and that is what the line
reports; "Signed in as" is reserved for a live authenticated client, which
means a game is actually online.

peek_instance is added for that: reporting state must not be the thing that
creates a client, or opening the settings screen would spawn the three RPCN
threads and immediately tear them down again.
2026-08-19 20:09:52 -04:00
jpolo1224 b27b61f86e Network: expose the rest of the core's Net settings, and saved RPCN servers
The Network tab offered three of the ten settings the core's Net node has.
The seven it did not were reachable only as raw core overrides, and one of
them is the setting that answers the question the tab exists to answer.

RPCN replaces Sony's PSN. A publisher's own backend never was PSN, so a game
that talked to one is not reachable through RPCN at all -- it is reachable by
resolving its hostnames somewhere else, which is DNS address and IP swap
list. Those had no writer anywhere in the app, so "use a custom server" had
no answer short of hand-editing config.yml. They are first in the tab now,
and their descriptions say which of the two layers they belong to, because
that distinction is the whole confusion.

Added with them: IP address, Bind address, PSN Country, Derive MAC from PSID
and Clans Enabled -- all six Settings.kt sites each, plus the bridge
dispatch, the setters, the search index and the tab's reset list. A key
missing from the dispatch is dropped in silence with no build error, so the
emitted keys and the handled keys were diffed rather than eyeballed.

EditableTextRow was dead ARMSX2 DEV9-era code with no callers, a hardcoded
"0.0.0.0" and no controllerFocusable, which would have made every one of
these rows unreachable on a pad. It takes a placeholder and a description now
and registers with the pad-nav registry.

Saved RPCN servers are a view onto cfg_rpcn's own "Hosts" list rather than a
second store, so add, remove, reset and anything the core does to it agree by
construction. Reset restores the shipped list and selects np.rpcs3.net, which
is the way back after typing a custom address over the top of it; removal of
the official entry is refused, matching upstream's own guard, and reported
instead of silently doing nothing. Host descriptions are user text being
pasted into a JSON field, so they are escaped on the way out.

Also exposes cfg_rpcn's experimental IPv6 support, which had no UI either,
and fixes the PSN row's label -- it still read "PSN (Simulated)" from when it
was a two-state toggle that could not select RPCN.
2026-08-19 19:50:51 -04:00
jpolo1224 b13bdd341b RPCN: sync the protocol to 31 and make the sign-in test work
Three things stood between the new RPCN account screen and the live server.

The client announced protocol 30, so np.rpcs3.net rejected every connection
with "Protocol Version Error (outdated RPCS3?)" before any credential was
looked at. Raising RPCN_PROTOCOL_VERSION alone is not enough -- the version
is a contract over the command enum, so the three commands added upstream
alongside it (GetRoomMemberDataExternalList, UnlockTrophy, SyncTrophies) have
to exist too, or every opcode after the insertion point shifts and the
mismatch shows up later as garbled replies instead of a clean refusal.

The other two were in the sign-in test, which was the one RPCN entry point
written standalone instead of through rpcn_with_connection.

It asked for the client with get_instance(0, true), and that flag does not
return an error: it calls fmt::throw_exception, which off a guest thread is a
fatal abort. Tapping "Test sign-in" before PSN status had reached the core
therefore killed the process. It reads like a validity check and behaves like
an assert, which is why desktop's get_rpcn_connection passes the default.

That state is easy to reach by accident, and not through user error:
Settings.applyTo() only runs at game boot or while a game is live, so
choosing RPCN from the library updates the UI and the store but leaves
config.yml on Disconnected until something boots. Testing an account does not
depend on that setting in the first place -- it needs credentials and a
reachable server -- so the check is dropped rather than deferred.

With the abort gone it hung instead, silently, with nothing in the log after
"Loading RPCN config". rpcn_thread's state machine only acts on want_conn
while disconnected, and wait_for_authentified sets want_auth alone: on a
fresh client the thread woke, found no connection request, broke back to its
semaphore and never released sem_authentified. Connecting first is what
np_handler does, and the four account operations already did it through the
shared helper. Sign-in now reports a real result either way.

Also correct a comment claiming cfg_rpcn stores a password hash. It does not;
set_password writes the string straight into rpcn.yml as plaintext.
2026-08-19 19:36:35 -04:00
digant73 e5e280cd3f Fix duplicate path addition 2026-08-20 00:27:30 +02:00
jpolo1224 7c6c26c210 Input: pace edges only, and never drop a release
The pacing added in 0.9 holds a button transition open long enough for the guest
to sample it. It ran on every write, and it could not tell a repeat from an edge.

Analog triggers write through it on every motion event -- dozens a second while
held, every one of them "pressed". Those repeats consumed the queue, so the
release that followed was the entry that reached TRANSITION_MAX_PENDING and got
silently dropped. Three symptoms, one cause: the queue replaying stale states 40ms
apart is the delay and the apparent double-press, and the dropped release is the
button that "presses itself and stays down until you press it again". Only
triggers showed it because only triggers stream same-state writes.

A repeat now returns immediately without touching the pacing budget, so a
trigger's pressure stays current and cannot starve its own release. And the
overflow branch no longer drops a release: losing a press costs one input, losing
a release leaves the pad holding a button the user let go of with nothing to clear
it. A late release beats a stuck one.

Reported against 0.9.2 in Ratchet & Clank (long jump is R2 held plus X), with 0.8
working -- 0.8 predates the pacing. The earlier guess at this, that a Retroid
trigger in "both" mode had two writers, was wrong; that guard is still correct on
its own terms but it was not what broke this.
2026-08-19 18:10:25 -04:00
jpolo1224 6b0d57b32e RPCN: make online reachable
The RPCN client has been compiled into this core the whole time -- rpcn_client,
rpcn_config, the localized login and account-creation error strings, 302 symbols
in the shipped library. None of it could be reached.

"PSN status" is a three-valued core setting (Disconnected, Simulated, RPCN) and
the port surfaced it as a BOOLEAN:

    setEnum("Net@@PSN status", if (enabled) "Simulated" else "Disconnected")

so np_psn_status::psn_rpcn had no writer anywhere in the app. And even had it
been selectable, an RPCN account needs an NPID, a password and a server, and
there was no screen to enter any of them -- grepping the whole UI for "npid"
returned nothing but two error-message strings. Online was not broken here, it
was unreachable, and anyone reporting "online doesn't work" was right in a way
nobody could act on.

Seven exports carry the account operations the core already implements: read and
write the config, create an account, resend the activation token, send a reset
token, reset the password, and test a sign-in. Each returns a finished sentence
on failure and an empty string on success, rather than an ErrorType or an
rpcn_state -- otherwise both enums would need a second copy on the Kotlin side,
kept in step by hand across a dlopen boundary that is explicitly allowed to
version-skew. They are resolved without ensure() like the frame-gen group, so a
core predating them degrades to "unavailable" instead of refusing to load.

The setting becomes a three-way picker, and psnStatus becomes an Int. Its readers
accept a stored Boolean so installs written before this read back as Simulated,
which is what true meant.

The account section appears only when RPCN is selected -- offering it otherwise
invites setting up an account the emulator will not use. It seeds from the core's
saved config, and the password field stays blank with its label saying one is
stored, because cfg_rpcn keeps a hash and there is no plaintext to show.

Every one of these blocks on the network and runs on Dispatchers.IO. Untested
against a live server: the flow matches what the desktop dialog does, but no
account has been created from a device yet.
2026-08-19 18:06:33 -04:00
jpolo1224 d0f9356199 PKG install: notice when it fails, and say why
Three faults on the same path, all of which turned a failed install into a
successful-looking one.

The extraction loop discarded what write() returned, and extract_success was
declared true and never assigned again -- the two occurrences in the file are its
declaration and the branch that reads it. So the failure branch was unreachable,
every file logged "Created file" whatever happened, and m_written_bytes counted
bytes INTENDED rather than written, which is why progress reaches 100% on an
install that did not complete. A short write now stops the file, logs what it got
against what it wanted, and marks the entry failed.

Nothing checked free space either. A full device gave one of two outcomes and
neither said "disk full": a hard abort out of ensure(r > 0) in fs::file::write,
or -- if the backend returned a short write instead of failing -- a truncated
file recorded as installed, which surfaces much later as a corrupt game nobody
can account for. The PKG header carries data_size, its own total for its
contents, so this preflight is exact rather than a floor; the 64MiB margin covers
directory entries and filesystem overhead that figure excludes.

Third, every error reached the UI as "Installation failed". app_version is not a
crash and not a bad file: it is the installer correctly refusing a game update
because the base game is not installed, or because the update does not match the
version that is. Desktop RPCS3 explains that in a dialog. Here it was
indistinguishable from a broken package, which is how a Tekken Tag 2 update came
in as an emulator bug report.
2026-08-19 17:42:42 -04:00
Megamouse c122edd638 unpkg: Fix buffer size checks 2026-08-19 23:30:34 +02:00
Megamouse d12d8782af unpkg: Mark pkg installation as failed if any thread throws an exception 2026-08-19 23:30:34 +02:00
jpolo1224 e73a40d9ce Input: one writer per trigger when a pad reports both ways
Retroid pads have an L2/R2 mode called "both": the trigger sends
KEYCODE_BUTTON_L2/R2 AND an analog axis. sendTrigger writes the pad button from
the axis, and the key path wrote it again -- one physical squeeze, two independent
writers on the same button, which arrives as a delayed or doubled press. Long
jumps in the Ratchet games hold R2 and were unreliable because of it; a reporter
had it in 0.9.2 and says 0.8 was fine.

sendTrigger already carries the opposite guard: a pad with no trigger axis at all
leaves "the key path in sole charge", added when Switch Pro controllers had their
held trigger cancelled by every stick movement. This is that guard's mirror, and
the two together mean exactly one writer owns a trigger on every pad -- the axis
where there is an axis, the key where there is not.

Keyed on the physical keycode before remapping, because what decides ownership is
how the hardware reports the trigger, not what the user bound it to.

Pads that report triggers ONLY as keys are unaffected: they have none of the three
axes, so the guard does not fire and the key path still owns them.
2026-08-19 17:27:42 -04:00
jpolo1224 5a34aeb06f Yakuza: also fix the FIFO desync, and get a host backtrace on Android
Past the loading screen the game hits a second, separate failure: the RSX FIFO
desyncs and reads a RET with an empty call stack -- 19 of them in one session,
last cmd 0x20000 every time. recover_fifo() resets it each time and eventually
gives up and kills the RSX thread ("Dead FIFO commands queue state"). The game
then sits at 0 fps with audio playing perfectly, because everything except the
renderer is still alive, which reads as a hang rather than a dead thread. The
semaphore acquires that time out alongside it are downstream: a desynced FIFO
never runs the release that would satisfy them.

Ordered & Atomic plus a 20us wake-up delay clears it. That is what the fatal
message itself recommends, and this is the first evidence here that the advice
is worth anything -- the same suggestion sits unanswered on two other reports.
Which of the two does the work is not established; both were changed at once and
it has not been A/B'd. Both cost performance, hence per-title rather than global.

Also: get_backtrace and get_backtrace_symbols were #ifndef ANDROID, so they
compiled to nothing on the only platform this port runs on. Every native fault so
far has had to be read out of a tombstone or symbolized by hand, and an access
violation gets neither -- that path freezes the emulator instead of aborting, so
the process survives and Android never writes one. Implemented with
_Unwind_Backtrace, which bionic always has, and dladdr for names; frames print
library-relative because the shipped .so is stripped and loaded at a random base,
and that offset is what llvm-symbolizer takes against the unstripped build.

The access violation handler now prints it. Yakuza reached that path once, reading
location 0xc on the RSX thread with the FIFO empty and parked at a self-jump, and
guest state alone could not say which of our functions dereferenced null.
2026-08-19 17:16:00 -04:00
jpolo1224 99a0fa5dd9 cellGame: refuse a game data install onto a full device
Nothing downstream checks free space. The game creates its directory, starts
copying, and the writes fail one at a time with nobody reading the return value --
so the install bar stops partway and sits there. That is indistinguishable from
the emulator hanging, which is exactly how it turned up: Yakuza Dead Souls stalled
at 78% on a device with 809MB free and produced not one line of log about it.

CELL_GAME_ERROR_NOSPACE is what the real system returns here and games already
handle it -- they put up their own "not enough space" dialog -- so this turns a
silent stall into the message the title was written to show.

The floor is 256MB and deliberately small. How much a game will write is not known
at this point (the size a title declares is advisory and many pass
CELL_GAME_SIZEKB_NOTCALC), so this cannot predict a failure; it only catches the
case where there was never a chance. An install that would have fit is unaffected.

Free space is logged either way, so a stall that is NOT this now says so.
2026-08-19 16:47:31 -04:00
jpolo1224 65454fef8e Yakuza Dead Souls: disable FIFO reordering
The game runs at 1fps with the flattener on, and not by being slow: the RSX blocks
on nv406e::semaphore_acquire until the wait times out, draws one frame, and does
it again. 145 timeouts in a single session, all on semaphore 0x50300FE0, while the
GPU itself was doing 3.06 ms of work per frame -- so roughly 997 ms of every frame
was the timeout. The acquire outruns the release that should satisfy it from the
very first frame: awaited 0x1 against an observed 0x0, and still 8 behind at 0x68.

Disabling reordering clears it completely and the game boots and plays.

The cause is NOT established, and the obvious candidate does not hold:
flattening_helper only drops registers carrying always_ignore, and that set is four
INVALIDATE methods with no semaphore in it -- a semaphore release takes the default
branch and flushes the batch, which is the safe path. So this is an empirical
per-title workaround and the mechanism is still open.

Both serials: BLUS30826 is the US disc, NPUB31509 the PSN release the original
report came from.

The STOCK entry is not optional. This port keeps one global config.yml, so a value
written for one game persists into the next; without the stock default, booting
Yakuza would leave FIFO reordering disabled for everything launched afterwards.
Same trap the Uncharted 3 entry documents.
2026-08-19 16:46:21 -04:00
kd-11 64da425e81 gl: Fix copy_image_static behavior when formats are mismatched 2026-08-19 20:43:05 +02:00
jpolo1224 2e65c8b212 PPU: drop the store-conditional failure probe
It did its job -- it is what identified res - rtime == 128 as the constant behind
the Assassin's Creed hang -- and what it costs now is an atomic increment on a
shared static every time any conditional store fails, on every PPU thread. That is
cross-core traffic on the hot path of every guest atomic, for a question that has
been answered.

The measurement it produced is kept in the comment above the fix, which is the part
worth having.
2026-08-19 14:04:44 -04:00
jpolo1224 91952ae4c1 Turnip: stop disabling fp16, add Space/Enter, make the IME actually appear
Three fixes found while testing Assassin's Creed on a Turnip device.

fp16 was off on every Turnip install regardless of driver age. The gate that
re-enables it compares driverVersion against 512.676.53, which is QUALCOMM'S
numbering; is_ADRENO() is true for Turnip as well, and Turnip reports Mesa's
scheme -- 25.99.99 on an 8 Gen 2 -- which packs to a far smaller integer and can
never pass. So the log said "All float16_t arithmetic will be emulated with
float32_t" on the configuration these handhelds actually ship in.

It should not have been gated for Turnip at all. The failure being worked around
is Qualcomm's shader compiler rejecting SPIR-V with float16_t in it; Mesa's is a
different compiler. The version check stays for the proprietary driver, where it
was measured, and Turnip passes on its own account.

The on-screen keyboard raised its bar without raising a keyboard.
showSoftInput(SHOW_IMPLICIT) is a hint the system may decline, and it does for a
fullscreen immersive window like the game surface -- but visible was set either
way, so the extra-keys bar appeared over nothing. It now also drives the IME
through WindowInsetsControllerCompat, which is the supported route once
setDecorFitsSystemWindows(false) is in effect, and it already is. Both are used:
the old call still works on odd IMEs and asking twice costs nothing. hide()
mirrors it.

Space and Enter join the bar as wide keys. They are on the IME too, but the IME
is not reliably what comes up, and Space is the key that opens the debug menu
this was built for -- it should not depend on another keyboard appearing. Enter
was already there as a glyph and read as decoration; it says Enter now.
2026-08-19 13:50:37 -04:00
jpolo1224 ef63026354 Keyboard: add the keys the Android IME does not have
The emulated keyboard reaches games now, and the first thing it was used for was
a game's debug menu -- which opened on Space and then could not be navigated,
because a soft keyboard is built for typing text and has no arrows, no Escape and
no function row. For that job those are not optional extras, they are the whole
interaction.

A row of them floats above the IME whenever the keyboard is up: Esc, Tab, the
four arrows, Enter, and an Fn toggle for F1-F12. It scrolls sideways so nothing
is cut off on a narrow screen, and it appears and disappears with the keyboard,
so there is nothing to place in the touch layout and nothing to find.

This adds to the IME rather than replacing it. Prediction, swipe and non-Latin
input still come from whichever keyboard the user chose; only the keys that
keyboard cannot express come from here.

Taps go through SoftKeyboard.tap, the same paced queue the IME's own keys use, so
a press is held long enough for the guest to sample it. The keys use tap gesture
detection rather than clickable(): this sits beside a focused IME sink, and
anything focusable here can take focus off it and drop the keyboard mid-use.

The arrows are KEYCODE_DPAD_*, which the handler already maps to Qt's arrow
codes, so nothing was needed on the native side.
2026-08-19 13:17:38 -04:00
jpolo1224 da4169148f Diagnostics: name the participants in a guest-side stall
Four probes, all sampled or already on a timer, added while chasing the
Assassin's Creed loading hang. Each one existed because a fact that turned out to
be decisive was unreachable from outside.

sys_event: EVENTSPIN counts sends and receives and prints one line per million of
each. The syscall usage stats already said two calls dominated everything; they
could not say which port, which queue, or whether the receive blocked. It was one
thread posting a zero-data event to its own queue and taking it straight back,
2.2 million times a second -- a yield loop, not a deadlock, which is a different
thing to go looking for.

The SPU half of the stall dump gains res_now/res_moved and the first bytes of the
line an SPU is parked on. The wait loop wakes on either the counter moving or the
data changing, so a sleeping SPU proves the line is static rather than that a
notification was lost. Those are different bugs and the dump could not tell them
apart.

It also gains one kernel's registers and the local store around its pc, matching
what the PPU half has always printed. The SPU's decision to sleep is guest code;
the registers hold what it tested and the local store holds the test.

The PPU half gains the memory behind registers of a spinning thread. dump_all
prints eight bytes, enough to recognise a pointer and not enough to read the
struct behind it -- the field that settled this one was 0x74 bytes past a value
in r5.

One SPU and six pointers, deduplicated and capped: a dozen parked threads each
dragging a hex dump per register buries the report that explains the hang.
2026-08-19 13:09:02 -04:00
jpolo1224 ca3b755fd1 PPU: advance rtime when a conditional store succeeds
ldarx has a fast path: re-reserving the same 128-byte line the thread's last
successful stdcx wrote skips reloading ppu.rtime, on the theory that the hardware
caches the reservation across a chain of stores. The branch is empty, so rtime
keeps the value it had before that store -- and the store left the line's counter
128 higher. Every conditional store after the first one on a line therefore fails
its rtime != (res & -128) test by exactly 128, and keeps failing, because a guest
that retries re-enters the same fast path with the same stale value.

Assassin's Creed never gets past its loading screen because of it. libsre's
cellSpursAddUrgentCommand walks four urgent-command slots inside a single
reservation loop: it stores slot 0 back unchanged to release it, then can never
claim slot 1, and its failure path restarts the whole scan. Measured at 490
million failures on 0x102ed6b8, res-rtime == 128 on every one, data unchanged.

Downstream of that, everything else looked like the bug and was not. The job
chain is valid (workloadId 4), three of its four urgent slots stay empty, no
workload is ever marked ready, all six SPURS SPU kernels sleep correctly on a
control block nothing writes, and the main thread burns 2.2 million syscalls a
second in a yield loop waiting for a load that cannot finish. One missing
increment, five symptoms.

047f71b43 added this fast path for MGS4 along with a "rtime -= 128" here and a
"rtime += 128" in the store; the two cancel, and both were later dropped rather
than corrected, which left the fast path reading a stale value. Advancing rtime
at the store is what makes the empty branch correct: it is already current.
2026-08-19 13:07:41 -04:00
jpolo1224 5c810c72c2 Make raw core overrides visible, and undoable
A raw override recorded in All Core Settings re-pushes at the tail of applyTo,
after the curated store has written the same node. So the recorded value wins
every time and the normal settings screen becomes decorative: it shows the
choice, saves the choice, and the choice is overwritten a moment later with
nothing on screen to say so. config.yml disagrees with the UI and there is no
way in the app to see why, or to undo it -- CoreSettingOverrides.clear() existed
and nothing called it.

That is not hypothetical. A test device carried

  Core@@PPU Decoder = "Recompiler (LLVM)"

which silently defeated every attempt to boot a game on the interpreter,
including a run made specifically to find out whether a hang was a codegen bug.
The run happened, the setting was chosen, and the log recorded the recompiler.

All Core Settings now reads the store alongside the tree: it says how many paths
this scope remembers, marks each row that carries one, and offers Forget per row
and for the scope. Forgetting also restores the node -- the core's default first,
then a curated re-apply on top -- because dropping only the record would leave
the value it wrote still live, which looks like the button doing nothing.

Reset is two taps rather than an AlertDialog: dialogs swallow gamepad keys here,
and this screen opens from the in-game menu with only a controller in hand.

The migration clears the paths a curated screen also writes. Three others --
RSX Profiler, PPU Calling History, Disable SPU GETLLAR Spin Optimization -- have
no curated writer, so they are actively written back to their upstream default
rather than merely forgotten; forgetting alone would remove the record and keep
the effect. All three are instrumentation or debug levers that were never meant
to ship on.

Accurate ZCULL stats is deliberately left alone: no curated writer and no
debugging history, so a value there is most likely a deliberate per-game choice.
It is visible and clearable now, which is the point.

Two earlier migrations already purged diagnostics by name, and both had run on
the device that still had RSX Profiler recorded. Hence a screen rather than a
third list.
2026-08-19 11:15:46 -04:00
jpolo1224 c1781b95cb Connect the keyboard to the emulator
The keyboard setting, the on-screen keyboard hotkey, the touch button and the
IME plumbing all worked. Nothing behind them did, in three separate places:

  - init_kb_handler installed NullKeyboardHandler unconditionally, so cellKb --
    the API games actually read a keyboard through -- reported none attached no
    matter what the UI said.
  - NativeApp.usbKeyboardKey and usbSetKeyboardEnabled were Unsupported.note()
    stubs. Every keystroke went into a no-op that returned false.
  - The setting wrote [USB1] Type = hidkbd, which is PCSX2's emulated USB HID
    keyboard. There is no such device in this core -- "hidkbd" appears nowhere
    in it -- so that write only ever reached Unsupported.note("USB1/Type").

All three are inherited from the UI port, which is why the feature looked whole.

The desktop handler is a QObject that installs an event filter on a QWindow, so
none of it survives the port. It does not need to: everything that turns a key
into cellKb data already lives in KeyboardHandlerBase::HandleKey, and a concrete
handler owes it exactly one thing, a populated qt_code -> CELL_KEYC map.
virtual_keyboard_handler builds that map with the Qt key codes as literals, and
translates Android keycodes onto it -- including left/right modifiers, which have
to come back in the native_key encoding get_out_key_code compares against or
every modifier reads as the right-hand one.

The setting now writes Input/Output/Keyboard, which is what the core reads. That
happens once, in Emulator::Load, so it applies on the next boot rather than to a
game already running; the description says so now, because the old comment
claimed a live attach that never existed.

KeyEvent.getUnicodeChar() is carried through as well. cellKb derives its own
character from the raw code plus the live modifier state and does not need it,
but the emulator's own overlay text entry matches on the string.
2026-08-19 10:39:08 -04:00
jpolo1224 fb5045d086 Build: keep the builder's absolute paths out of the binary
__FILE__ expands to whatever path the compiler was handed, and RPCS3 prints source locations in
ensure() failures, fmt::throw_exception and assertions. Every one of those lines therefore carried
the full build directory into EVERY USER'S LOG -- on a developer machine that is a home directory,
and the shipped core contained 2500 copies of one username. It surfaced in a bug report where a
user's log showed somebody else's paths, which is a reasonable thing to be alarmed by.

-ffile-prefix-map rewrites the prefix at compile time, covering both __FILE__ and debug info, so
paths become relative-looking (./rpcs3/Emu/...) -- which is what a log wants to show anyway. No
runtime cost.

Applied before any add_subdirectory so third-party targets built in-tree are covered too: they
account for 32k of the roughly 52k embedded paths. Guarded with check_cxx_compiler_flag so a
toolchain without it still builds.
2026-08-19 09:04:13 -04:00
kd-11 b78bae0b9f rsx: Integrate ROP remapping to the interpreter system
- Base pipelines have the remapping active, optimized variants can toggle it away.
2026-08-19 14:08:55 +03:00
kd-11 ca25fbaa5d vk/gl: Support ROP output remap in the interpreter 2026-08-19 14:08:55 +03:00
kd-11 323e2d35a2 rsx/fp: Implement support for ROP_OUTPUT_REMAP in the backends 2026-08-19 14:08:55 +03:00
kd-11 57ecb42433 rsx/fp: Plumb through support for channel remapping during ROP 2026-08-19 14:08:55 +03:00
kd-11 84ceef9717 rsx/glsl: Shrink command space in ROP_CONTROL structure to make room 2026-08-19 14:08:55 +03:00
kd-11 0dd3d6528c rsx/prog: Move in-shader color remap to FP prolog
- We need it for some other stuff
2026-08-19 14:08:55 +03:00
Antonino Di Guardo ddd82ecada Make optional VSH on File âž” All Titles âž” Create LLVM Caches dialog (#19270)
Make optional VSH CPU compilation on `File âž” All Titles âž” Create LLVM
Caches` dialog. By default it is now excluded due to VSH caches are
fully covered by the dedicated submenu `File âž” Firmware`.
2026-08-19 08:15:27 +02:00
jpolo1224 4c080066cf Save data: fix archive import always failing, and stop mangling names
Two bugs, both mine, both in the new importer. Reported as "import failed, could not open
the zip".

The archive path could never succeed. These stage functions answer null to mean "no problem,
carry on", and importArchive folded the open into the same expression:

    contentResolver.openInputStream(uri)?.use { stageArchive(...) }
        ?: Outcome(false, "Could not open the selected file")

so a null from stageArchive -- the SUCCESS return -- selected the elvis branch. Every archive
import reported a file it had in fact opened and read as unopenable, and returned before
discover() ever looked at what had been staged. The open is now checked on its own, so the
two cases cannot be confused again; the message was pointing at the wrong thing, which is
what sent the diagnosis into the zip handling.

Separately the file carried six NUL bytes where space character literals were meant:
trimEnd('\0'), it == '\0'. They compile -- '\0' is a valid Char -- so nothing complained, and
grep silently reports nothing for a file it decides is binary, which is why searching for
these came back empty on a file that plainly contained them.

Fixing them corrects the rule rather than restoring the intent, because the intent was wrong.
Spaces are ordinary in a downloaded folder name: "All Pro Football 2K8 roster/" is exactly
what a file manager produces. Stripping them silently renamed the user's folders, and the SAF
walk skipped any entry containing one, so a wrapper folder made its contents invisible. Source
names now reject only separators and control characters.

The destination name is no longer narrowed to a character set either. It comes from the game's
own SAVEDATA_DIRECTORY, and refusing one for holding an unanticipated character would produce
"no save data found" -- the same silent-looking failure this class exists to prevent. Only
separators, control characters and a leading dot are rejected, since those are what can
redirect a write or hide the result.
2026-08-18 18:16:03 -04:00
jpolo1224 044ba9cb03 Save data: import a save or roster from a picked folder or .zip
Android 11 stopped third-party file managers from writing into Android/data, so a user who
downloads a save cannot put it where the emulator reads from -- ZArchiver reports EACCES and
no file manager can do better. Reported against an All Pro Football 2K8 roster on an Ayn Thor
Pro. The platform rule is not ours to fix, but we are the only process that can still write
there and until now offered no way to ask us to, so there was no route in at all.

The destination folder name comes from the save's own PARAM.SFO rather than from what the
user's folder or archive is called. Games enumerate saves by matching dirNamePrefix against
the directory name, so a save under the wrong name produces no error the user ever sees --
the game just reports no save data and offers to start fresh, which reads as "the import did
nothing". The core writes SAVEDATA_DIRECTORY into every PARAM.SFO it saves and reads it back
to populate dirName, so the name that works travels inside the save; a renamed download still
lands correctly. The folder's own name is the fallback, and both go through the same
sanitiser because a value read out of a file is untrusted wherever it came from.

Staging follows TexturePackInstaller: everything is copied to a scratch directory beside the
destination, checked there, and only then renamed in, so a cancelled or failed import cannot
leave a half-written save and cannot destroy one the user already had. Archive entries are
rebuilt from sanitised components and any entry containing a '..' fails the whole archive --
this writes into app-private storage, which includes the native library directory, so a
zip-slip here is code execution rather than untidiness. Extracted sizes are counted while
writing rather than trusted from the entry header.

Two rows because the two pickers are different intents: an archive straight from a download,
or an already-unzipped folder. Both accept being pointed at either the save itself or a
parent holding several, since a user has no reason to know which they picked.

stageArchive/stageTree/commit are deliberately not savedata-specific -- the frame generation
plugin installer needs the same component (pick a file, verify it, atomically place it
somewhere the app owns) and should lift these rather than grow a second copy.
2026-08-18 18:16:03 -04:00
jpolo1224 5dee5bc42e Release: 0.9.1 (versionCode 16) 2026-08-18 18:16:03 -04:00
jpolo1224 1b9ab35ac0 Docs: record the ANGLE prebuilts in the jniLibs table
They are the one entry in that directory tracked in git rather than staged build output,
which is why .gitignore un-ignores them by name and verifyAngleLibs fails the build without
them.
2026-08-18 18:16:03 -04:00
jpolo1224 26c39e1f55 Input: only report player 1 connected until a port is actually driven
0.9 shipped seven permanently-connected pads. Reported against LittleBigPlanet 2, which
reacts to the connected count and behaved as though 4+ controllers were plugged in at all
times.

Claiming all seven ports for the virtual handler is what lets a second controller work at
all -- that was the 0.9 fix and it stays -- but initVirtualPad called Init with
CELL_PAD_STATUS_CONNECTED unconditionally, and "this port exists" and "a controller is
plugged into this port" are not the same statement. cellPad derives now_connect by counting
ports whose status carries the CONNECTED bit, so every game asking how many pads were
attached was told seven.

Port 0 therefore starts CONNECTED and ports 1-6 start 0, with the bit set on a port the
first time real input arrives for it in _rpcsx_overlayPadData. No new event plumbing is
needed: pad_thread::update_pad_states already polls is_connected() against its cached
m_pads_connected -- value-initialised to false, so a port that starts disconnected fires
nothing at boot -- and calls pad_state_notify_state_change on a change, which is what
propagates m_port_status into cellPad's reported_info. Setting the bit is enough for the
existing path to publish it.

m_player_id is a const set at construction, not by Init, so reading it to pick the initial
status is safe at this point.
2026-08-18 18:16:03 -04:00
jpolo1224 8bc7ca307c Update README.md 2026-08-18 16:30:45 -04:00
Ani 7973b8ac6d windows: Fix clang x64/arm64 builds 2026-08-18 21:54:09 +02:00
jpolo1224 93110e899f Release: 0.9 (versionCode 15) 2026-08-18 15:09:55 -04:00
jpolo1224 4b27b585f5 Input: let analog triggers bind in the pad-button capture
On a pad that reports L2/R2 as AXES rather than buttons they arrive in
dispatchGenericMotionEvent and never as a key, so the binder -- which lives in Compose's
onPreviewKeyEvent -- could not see them, and L2/R2 would not bind while every other button on
the same pad did. That depends on the controller MODEL, not the port, which is what makes it
present as a Player 2 problem: a second pad of a different make fails where the first worked.
Reported against Player 2 with everything else binding.

handleCaptureMotion already existed for exactly this shape -- the D-pad HAT arrives as an axis
on several handhelds and is synthesised into a key during capture -- so the triggers just join
its  set and inherit its press/release tracking, which debounces the motion stream for
free.

Uses the same axis pairs as the gameplay path (sendTrigger), including the per-device third axis
some pads put the right trigger on, so a trigger that works in game can also be bound.
Threshold is a deliberate half-pull so resting drift on a worn trigger cannot self-bind.
2026-08-18 14:31:22 -04:00
jpolo1224 bbf44c684c Input: pace digital transitions so a short press cannot fall between guest polls
The guest polls cellPad at its own rate -- 33ms at 30fps -- and a press plus its release are
two separate snapshot pushes with nothing between them. A press shorter than one poll interval
lands entirely between polls and the game never sees it. Steady presses always span a poll,
which is why a button works everywhere except during a rapid mash, and why a held button
(crouch) keeps working while everything tapped alongside it does not.

GestureLayer already knew this and worked around it locally with a 40ms hold in pulse();
ordinary touch taps and physical controller presses had no equivalent. Reported on Iron Man's
quick-time event, where circle must be pressed repeatedly and does not register, and again as
'only the crouch button works' with both on-screen controls and a PS5 pad.

Delaying a too-short release would NOT fix it and would look identical: in a mash, press N's
deferred release collides with press N+1 and the game sees one long press instead of several,
while a QTE counts presses. Each transition therefore gets its own slot -- press visible for
40ms, release visible for 40ms, then the next press -- so a mash arrives as distinct presses
rather than a hold.

A press with nothing queued ahead of it still goes through immediately, so normal input takes
no added latency; only the release of a tap shorter than 40ms is deferred, and only up to 40ms.
Queue depth is capped so a mash cannot accumulate while the guest is not consuming.

40ms matches the gesture path and clears one 60Hz sample. NEEDS FIGHTING-GAME TESTING: it also
paces the d-pad, and 40ms is about 2.4 frames at 60fps, so frame-tight directional input is the
case most likely to feel different.
2026-08-18 14:23:15 -04:00
jpolo1224 f823f6fd66 Touch: list the keyboard button in the default layout so the editor offers it
Adding the TouchButtonId was not enough. The editor offers what the LAYOUT contains, not what
the enum declares, so the button existed and did nothing visible -- reported straight away.

Listed with enabled = false alongside the save/load/screenshot buttons, which is the same
opt-in shape. Existing layouts pick it up without being disturbed: TouchLayout.fromJson
splices in any default button a saved layout lacks, and defaultPortrait splices from this
same table, so one entry covers both orientations and everyone's current layouts.
2026-08-18 13:56:34 -04:00
jpolo1224 9a12b5e958 Pads 2-7 at startup, and an on-screen keyboard button
Ports 2-7 were left as Null pad handlers unless a USB device happened to be plugged in. The
loop that claims them for the virtual handler existed, but only inside _rpcsx_usbDeviceEvent,
so it ran on a USB plug/unplug and nowhere else. A second controller on a phone is normally
BLUETOOTH, which never produces that event, so those ports stayed Null and a second pad did
not exist in the core at all. Per-player button mapping therefore looked correct -- the UI
stores those bindings regardless -- while the second controller did nothing in game. Reported
against Tekken 6. Now claimed at startup, next to player 1.

Also adds a KEYBOARD touch button (Kind.STATEACTION, so it emits no pad code and calls
MainActivityRuntime.toggleSoftKeyboard) for the same reason the hotkey exists: to reach the
keyboard without pausing. The hotkey needs a spare pad button, which a touch-only player does
not have. Opt-in, absent from the default layout, like the save/load/screenshot buttons.

Appending to TouchButtonId is safe: touch layouts serialise the id by NAME
(TouchButtonId.valueOf), unlike SysHotkey which is persisted by ordinal.
2026-08-18 13:43:32 -04:00
jpolo1224 d094ecd491 Settings: put Emulate USB Keyboard in the Network tab
It existed only in the in-game pause menu, which made the On-Screen Keyboard hotkey's own
message a dead end: it tells you to turn this on in Network settings, and there was nothing
in Network settings to turn on. Reported from Discord after exactly that.

Same Settings.usbKeyboard field as the in-game row, so the two stay in sync, and indexed for
settings search.
2026-08-18 13:29:42 -04:00
jpolo1224 fda4cc3b50 Revert the added keyboard work: it duplicated an existing feature and shifted hotkey ordinals
Reverts 422d831ef and 00ce69a31.

ARMSX3 already had all of this. Settings.usbKeyboard writes USB1/Type = hidkbd and
NativeApp.usbSetKeyboardEnabled, the TOGGLE_KEYBOARD hotkey raises the Android IME through
SoftKeyboard.toggle, and dispatchKeyEvent already forwards keys via forwardKeyToUsbKeyboard.
The toast users see -- "Turn on Emulate USB Keyboard (Network settings) first" -- is that
feature correctly reporting that its setting is off, not a missing capability. What I added
was a second, parallel path through cellKb with its own setting and its own hotkey.

The revert is not only for redundancy. SysHotkey is persisted BY ORDINAL, as the comments
around TOGGLE_KEYBOARD and GYRO_RECENTER say in as many words, and both are appended last
for exactly that reason. KEYBOARD_TOGGLE was inserted mid-enum, ahead of GYRO_TOGGLE, which
re-points every binding after it for every existing user.
2026-08-18 13:21:57 -04:00
jpolo1224 00ce69a315 Android: on-screen keyboard over the running game, and a setting to enable it
Uses the Android system IME rather than a drawn key grid, so layouts, languages, prediction
and emoji come for free and it is the keyboard users already know. Bound to a new
KEYBOARD_TOGGLE hotkey, so it can be raised and dismissed mid-game without opening
settings.

The IME only opens for a focused view that accepts input, so a zero-size transparent
EditText owns focus on demand. Its InputConnection does the real work, because an IME
reports typing in two different ways and only one of them is a key event:

  sendKeyEvent            backspace, enter, arrows -- forward the keycode as-is
  commitText              ordinary characters, with NO key event behind them
  deleteSurroundingText   some IMEs delete by range instead of sending backspace

commitText is synthesised with KeyCharacterMap.getEvents, which produces the shift presses
capitals and symbols need rather than guessing a keycode per character. Characters no
keycode can produce -- emoji, CJK picked from a candidate list -- are still delivered with
their unicode and KEYCODE_UNKNOWN, since the guest reads the unicode field and that is the
honest keycode for a character with no key behind it.

Also adds the Emulated Keyboard setting, without which all of this was inert: the core
defaults to keyboard_handler::null, so cellKb told games no keyboard was attached no matter
what was typed. Off by default, matching the core, because a game that sees a keyboard can
behave differently. The hotkey says so in its toast when the guest cannot receive keys,
rather than silently showing an IME that goes nowhere.
2026-08-18 13:09:51 -04:00
jpolo1224 422d831eff Android: give the guest a real keyboard
cellKb reported no keyboard at all, so games that need one were unreachable: NFS Most
Wanted's beta debug menu, and native keyboard support in games like Counter-Strike. The
only handler upstream ships, basic_keyboard_handler, derives from QObject and filters
QKeyEvent off a QWindow, and android/CMakeLists.txt excludes it with the rest of the Qt
input layer -- init_kb_handler was hardcoded to NullKeyboardHandler as a result.

Almost none of that handler is actually Qt-bound. KeyboardHandlerBase::HandleKey already
takes plain u32 codes and keyboard_consumer::ConsumeKey resolves them through
m_keys.find(code), so the code space only has to agree between whatever registers the
buttons and whatever injects them. android_keyboard_handler therefore registers ANDROID
KeyEvent keycodes directly rather than impersonating Qt. The PS3 side uses USB HID usage
IDs and Android's letters and digits are contiguous too, so those map arithmetically and
only the remainder needs a table. Android also distinguishes left from right modifiers,
which Qt cannot, so all eight are wired rather than four.

init_kb_handler now honours the Keyboard setting instead of always reporting none, and
_rpcsx_keyboardKey delivers one key through the usual dlsym bridge, returning false when
no keyboard is active so a caller can tell the difference.

A physical keyboard reaches the guest through dispatchKeyEvent. The test there is
KEYBOARD_TYPE_ALPHABETIC, not the event source: gamepads also report SOURCE_KEYBOARD for
their buttons, so filtering on source alone would send every controller press to the guest
keyboard as well as the pad. The event is consumed only when the native side reports the
key landed, which keeps a physical keyboard usable for UI navigation everywhere else.

Not yet done: the on-screen keyboard overlay, and a UI setting for the handler. The core
default is still keyboard_handler::null, so this is inert until Keyboard is set to Basic.
2026-08-18 12:57:34 -04:00
jpolo1224 55a54c924e SPU/Android: stop generated SPU code running off the end of the thread stack
The ARM64 SPU gateway reserved a shared 8192-byte stack scratchpad. Compiled SPU
functions build no frames of their own on ARM64 -- GHC_frame_preservation_pass runs with
use_stack_frames = false -- so every one of them spills into that single reservation, and
a function needing more simply writes past it. Borderlands 2's 2401-instruction function
at LS 0x25da8 wants ~21 KB: the fault landed at sp+21760, exactly the top of the thread's
stack mapping, on the PROT_NONE guard page above it. x86 reserves 0xc8 in the same place
because LLVM emits ordinary per-function frames there, so this arrangement and this
failure are ARM64-only. Raised to 256 KB.

That is still a fixed bound rather than a scaling fix; a larger function could overflow
it the same way. use_stack_frames = true would scale, at a cost the pass comments call
out and which is not measured here.

Android threads also ran on an eighth of the stack they get elsewhere: the pthread path
passed null attributes, so bionic's 1 MB default applied where glibc gives 8 MB, measured
as a 0xfc000 stack mapping. Not the cause of this bug -- the overrun is off the TOP of the
stack, so size does not affect it, and 1 MB to 64 MB changed nothing -- but a real
discrepancy worth closing.

Both were invisible because of how the fault died. A guard page is not emulator memory,
so is_emulator_fault() correctly declines it, the handler forwards to libsigchain, and
ART's FaultManager reads the guest registers as an ArtMethod* and takes the process down.
No tombstone is produced, the async emulator log never reaches disk, and Android records
only 'SIGNALED status=11'.

Verified with the function compiled and no forced interpretation: zero stalls, zero
guard-page faults, 47 presented frames where the previous best was 18.
2026-08-18 12:19:07 -04:00
jpolo1224 19d23eb691 SPU LLVM: fix ARM64 SHUFB byteswap fold and accurate-xfloat CFLTS
SHUFB: a7fc31f32 made two semantic changes to the ARM64 path, and BOTH have to go. It
widened the byteswap fold from splat-only constants to any constant, byte-reversing
non-splat ones in get_swap_from_const, and it added idx_selects_single, which treats a
mask whose bit 4 is known-constant across all lanes as single-source.

Borderlands 2's SPURS function at LS 0x25da8 is 1446 shufb whose data operand is usually
a non-splat constant -- 0xbf800000 built by ilhu/iohl, or a mask straight out of cbd/cwd.
Compiled, that function spins forever inside a single block: block_counter, loop count
and retreat count are byte-identical across six thread dumps spanning the hang, at 96%
CPU, so it never reaches a block boundary. Interpreted, the game boots.

Reverting the byteswap widening alone is NOT enough -- measured, and the hang came back
with seven stall dumps at 0x25da8. Disabling the whole ARM64 shufb block in favour of the
generic path also fixes it, which is what identifies the fold and the single-source
trigger rather than the tbl/tbx paths themselves. Kept narrow so ARM64 keeps its fast
paths.

CFLTS accurate xfloat: only the high side was guarded. The f32 path is a single saturating
fcvtzs.4s, but this one converts f64[4], and AArch64 has no v4f64->v4i32 form, so it
lowers to fcvtzs.2d twice plus uzp1 -- saturation happens at int64 range and uzp1 then
keeps the low 32 bits. Negative overflow therefore did not produce 0x80000000: -3e9 came
back as +1295786496.
2026-08-18 12:19:06 -04:00
jpolo1224 884cb47dde SPU: fix ARM64 float-to-int conversions in the interpreter
CFLTS and CFLTU both carried x86 corrections that are wrong on AArch64, and the
SSE templates they live in are what spu_interpreter_rt is built from, so they are
live on ARM64 through spu_run_interp_fallback.

CFLTS applied the cvttps2dq fixup: x86 returns the integer-indefinite value
0x80000000 for anything unrepresentable, positive overflow included, so the result
was XORed back. _mm_cvttps_epi32 is sse2neon's vcvtq_s32_f32 (FCVTZS), which
already saturates, so the correction inverted a correct result. Measured: +3e9 gave
0x80000000 instead of 0x7fffffff, and NaN gave 0 instead of 0x80000000.

CFLTU went further and relied on the 0x80000000 return, ORing the remainder back in
to rebuild the u32. On ARM64 the conversion yields 0x7fffffff, and 0x7fffffff | v is
0x7fffffff for every v below 2^31, so the entire upper half of the range collapsed to
one value: 3e9 read back as 0x7fffffff rather than 0xb2d05e00.

This also matters for diagnosis, not just correctness: forcing a block to the
interpreter is the standard test for whether the recompiler emits wrong code, and
until now that test could introduce a fault the recompiler did not have.
2026-08-18 10:40:00 -04:00
FlexBy420 3c15df4e4d Update sceNpTrophy.cpp 2026-08-18 14:19:42 +02:00
jpolo1224 9b33316982 SPU: copy the reservation line 16 bytes at a time on ARM64, and add SPURS dispatch diagnostics
mov_rdata and mov_rdata_nt move the 128-byte reservation line -- the GETLLAR
snapshot, and the fill back into live guest local store. On x86 that is four
16-byte vector moves, so each quarter lands whole and a racing reader sees either
the old or the new 16 bytes. On ARM64 both fell through to std::memcpy, whose
granularity is a libc implementation detail; AArch64 implementations mix transfer
sizes freely, so a reader can observe a line stitched from both versions. Use
eight vld1q_u8/vst1q_u8 pairs to match what x86 gets for free.

This does NOT fix the Borderlands 2 hang -- measured, no change to any observable:
same 4807 SPU blocks, same 0x29b48 ceiling, same stall state. It is committed as a
latent correctness fix rather than a behavioural one: the copy exists to produce a
coherent snapshot and had no atomicity guarantee here at all.

The diagnostics are the instrumentation that traced that hang from symptom to a
single missing DMA: guest thread and thread-group state at an RSX stall, per-SPU
conditional-store counters, the local-store and reservation-vs-memory dumps, code
GET destinations, the SPURS control-block fields, and the register dump at the last
transfer both hosts issue in common. They hang off the existing rate-limited stall
report or are capped by distinct key, because every earlier attempt at this was
capped by volume and got eaten by whichever event happened most often.
2026-08-18 08:10:43 -04:00
kd-11 719cf8a54a gl: Fix build warning 2026-08-18 13:47:28 +03:00
kd-11 1b879360b8 rsx: Fix OOB section writes generated due to mipmap dimension clamping 2026-08-18 13:47:28 +03:00
kd-11 ad059d03af vk: Avoid redundant copy when writing to mip level or Z layer 2026-08-18 13:47:28 +03:00
kd-11 78ba581137 vk: Extend copy image API to support 3D offsets and extents 2026-08-18 13:47:28 +03:00
kd-11 cd044148be gl: Avoid redundant copies when copying to mipmaps or 3D slices 2026-08-18 13:47:28 +03:00
kd-11 91cd82a0c2 gl: Extend copy image API to allow explicit 3D offsets 2026-08-18 13:47:28 +03:00
kd-11 e7c3d6ab26 gl: Implement support for explicit multi-layer image copy operations 2026-08-18 13:47:28 +03:00
kd-11 a88331bb22 rsx/vk: Implement support for multi-layer, multi-level and explicit mip/layer image transfers 2026-08-18 13:47:28 +03:00
kd-11 6892ee4c2e rsx: Fix mipmap gather source offsets 2026-08-18 13:47:28 +03:00
Megamouse 0059e4e92e unpkg: fix OOB read at end of file
Use sizeof(u128) instead of 16.
Clear padding after archive_read_block.
Use aligned_div instead of manually aligning blocks.
Fix local_buf size when using raw ptr of the original buffer.
2026-08-18 10:20:30 +02:00
Megamouse 46b7428fad unpkg: also check potential overflow in pkg data size check 2026-08-18 10:20:30 +02:00
Megamouse 582b5ba29e unpkg: fix OOB memset, use safe versions of write_to_ptr and read_from_ptr 2026-08-18 10:20:30 +02:00
FlexBy420 cb175278b6 RPCN: Sync trophies (#18760)
Add synching local trophies with RPCN server trophies, allows users to
essentially cloud save their trophies when they are connected to RPCN.

This PR also aims to add support for global earned % trophies simillar
to steam for RPCN part of website.

No local trophies and not connected to RPCN
<img width="1183" height="717" alt="obraz"
src="https://github.com/user-attachments/assets/f0b3add1-506d-43e5-8dae-cd34e041e2bd"
/>
After booting the game with RPCN enabled
<img width="1184" height="713" alt="obraz"
src="https://github.com/user-attachments/assets/d5992ddc-7fad-4b38-b4f9-006a832ffed5"
/>

RPCN Side PR
https://github.com/RipleyTom/rpcn/pull/140
2026-08-18 06:54:51 +02:00
yahfz 82164a54c1 [SPU LLVM] Avoid redundant XFloat normalization in SELB 2026-08-18 03:48:46 +02:00
Malcolmandjpolo1224 6161ecd7aa PPU: Stop inverting float-to-int saturation on ARM64
- This was breaking armored core 4/4a on ARM
- Original fix credit to jpolo1224 of armsx3 https://github.com/ARMSX2/ARMSX3/commit/87ccdb85152a707f5d0c8122ed39344b4648501e

Co-authored-by: jpolo1224 <jpolo1224@gmail.com>
2026-08-18 03:12:13 +02:00
jpolo1224 55a35b5e1d Diagnostics: guest-thread stall reporting, SPU reservation counters, autotest harness
Hangs where the RSX idles were only ever visible from the RSX side, so a stall
report now names every guest thread, its state, PC and function, and for SPUs adds
the reservation counters -- conditional store calls, failures, notifications, and
the SPURS heuristic's deliberate non-notifications -- plus where the host thread
last was in cpu_task. block_counter alone cannot separate a thread livelocked
retrying PUTLLC from one that is genuinely idle; both report zero blocks a second.

The SPU code window prints once per process. Unguarded it re-emitted a whole
function on every stall dump, measured at 538 lines a second over 31 dumps with a
690 MiB log left behind, which on Android is itself a stall -- it was degrading the
hang it was meant to describe, and it buried the state lines that answered the
question.

do_local_task counters cover the case the profiler cannot: it reports the thread is
in Local task and has been for 0.00s, which together mean it is not stuck there at
all and the FIFO loop is calling it repeatedly. Which FIFO state, and whether guest
GET equals PUT, separates a starved RSX from a stuck one.

tools/ps3autotests drives ps3autotests on a device over adb and diffs per
instruction against real-hardware output; compare-platforms.py does the three-way
ARM/x86/hardware split that separates shared upstream failures from ARM-only ones.
This is what found the CFLTS and FMS divergences.
2026-08-17 20:50:56 -04:00
jpolo1224 8caacc8231 Android: correct persisted off-spec settings, file:// launches, and RSS reporting
Accurate SPU Reservations was persisted false in the global config, left over from
earlier debugging, where upstream and our own defaults are both true. Turning the
default back on reached nobody who had already run the app, so this migrates the
stored value -- correcting the curated field and forgetting the raw override at
global scope only, since a per-title exception exists on purpose and
forgetEverywhere() would take it with it. Save LLVM logs had the same problem and
needed the value recorded, not just the override un-pinned.

A file:// launch never booted: the intent path was passed through as a URI string
and the loader wants a filesystem path, so only content:// ever worked.

get_memory_usage() reports system-wide totals -- MemTotal minus MemAvailable, every
process on the machine plus page cache -- and was being read as if it were ours.
Add get_process_memory_usage() for this process's resident set, which is the number
Android's low-memory killer actually decides on, and report that instead.
2026-08-17 20:50:43 -04:00
jpolo1224 0ded153216 Settings: add the console System settings, and route PS3/System to the core
Console Language, Keyboard Type, Console Region, Date Format, Time Format and
Enter Button Assignment had no UI, so anything the core read from PS3/System was
whatever the default happened to be.

Enter Button Assignment in particular was already a field but never reached the
core: Rpcs3Bridge.setSetting has no fallthrough, it translates a fixed set of
(section, key) pairs and silently drops the rest, and PS3/System was not among
them. Add the branch, add the five missing fields through the eight sites a
Settings field needs, and give them setters that map an index to the enum name --
these serialise by name, and the enum is neither contiguous nor in formatter order,
so an index cannot be written straight through.
2026-08-17 20:50:26 -04:00
jpolo1224 4baefed106 Android: install fault handlers ahead of the ART runtime, and handle SIGBUS
Calling sigaction() on Android does not make you the first handler for SIGSEGV.
libsigchain intercepts it and runs ART's FaultManager first, which reads the
faulting thread's registers as an ArtMethod* and dies on guest data -- so every
recoverable guest fault in JIT'd code killed the process before our handler ran,
with nothing in the app log to say why. Resolve sigaction from libc directly and
install through that, keeping the runtime's previous action so non-emulator faults
are forwarded on rather than swallowed. Registering through both paths recurses,
so this registers once and guards re-entry.

SIGBUS was only handled on Apple platforms; Android raises it for the same
unmapped-guest-page cases, so it needs the same treatment.

Also make the fault report survive a fault taken while reporting: emit an
allocation-free breadcrumb with the signal, address, PC and the GPRs before the
formatted dump, and chain to the previous handler first so debuggerd still records
a tombstone. The breadcrumb goes after the recovery attempts, not before -- emitting
it on entry logged over a thousand recovered faults per second and was itself a
stall.
2026-08-17 20:50:17 -04:00
jpolo1224 424514fde6 SPU: fix two ARM64 float divergences and make the object cache key cover codegen
CFLTS applied an x86 saturation correction on every host. cvttps2dq returns the
integer-indefinite value 0x80000000 for anything it cannot represent, positive
overflow included, so XOR-ing all the bits when the input is >= 2^31 produces the
0x7fffffff CFLTS wants. AArch64's FCVTZS already saturates that way, so the same
XOR turned a correct saturated-high result into saturated-low, and its NaN-to-0
conversion became 0xffffffff where x86 lands on 0x7fffffff. Same shape as the
FCTIW/FCTIWZ/FCTID split already guarded in PPUTranslator; the SPU one was missed.

FMS expressed a * b - c as fma(a, b, -c). x86 folds that into vfmsub and never
materialises -c, so a NaN addend propagates its own bits; AArch64 cannot take that
shape -- FMLS is Zd - Zn*Zm -- so it emits the FNEG and propagated the negated NaN.
0x7fffffff is not a NaN on a real SPU, just a large number, so the two hosts
disagreed about the sign of a huge result. Negate the addend only when it is not a
NaN pattern, with a known-never-NaN early out to keep it off the common path.

Measured with ps3autotests cpu/spu_fpu against x86 output from an otherwise
identical build: cflts 16 -> 0 differing lines, fms 484 -> 0, and spu_fpu as a
whole 984 -> 0 against a non-AVX512 x86 host. The 484 fma lines that remain
against an AVX-512 host are that host's vfixupimmps path and reproduce on any x86
without AVX-512, so they are not ARM-specific.

The cache key hashed the build stamp of SPUCommonRecompiler.cpp while the code
generator lives in SPULLVMRecompiler.cpp, so editing codegen alone did not move the
key and a rebuilt emulator silently reused objects from the previous binary -- the
first attempt at the CFLTS fix looked like it did nothing for exactly that reason.
Export a stamp from the codegen TU and hash that in as well, and prune stale
spuobj-* siblings so a version bump does not strand old directories.
2026-08-17 20:50:07 -04:00
Megamouse de33fda28c rsx_debugger: fix g8b8 conversion 2026-08-17 22:42:05 +02:00
jpolo1224 37a5d118be Merge PR #64: Library: a filename-derived PS3 serial gets a hyphen and loses its cover 2026-08-17 13:14:01 -04:00
jpolo1224 301f45a2cb Merge PR #63: cellAudio: don't let a silent port reset the untouched baseline every period 2026-08-17 13:14:00 -04:00
jpolo1224 aa25da4ce2 Merge PR #62: Stop two guest polling loops from flooding the log 2026-08-17 13:13:59 -04:00
jpolo1224 e35bd463cf Merge PR #60: Bundle the H.A.W.X. 2 Bink overlay patch and enable it 2026-08-17 13:13:57 -04:00
Neil Monday f9f88aa9e5 Use max() to bring negative floats up to 0.0 before uint conversion. 2026-08-17 15:17:17 +02:00
Zulux91 968b892e29 Library: a filename-derived PS3 serial gets a hyphen and loses its cover
FilenameParser reconstructs every serial in the PS2 dump shape -- four letters,
a hyphen, five digits (SLUS-20312) -- because that is the convention its regex
was written for. A PS3 title ID has no separator, so a game whose serial comes
off the filename rather than the disc is recorded as BLUS-30917.

That serial matches nothing. Cover art is fetched as COV/<TITLE_ID>.JPG, keyed
by exactly the id PARAM.SFO gives us, and the extracted-icon fallback is
disc-icons/<TITLE_ID>.png:

  COV/BLUS30917.JPG  -> HTTP 200
  COV/BLUS-30917.JPG -> HTTP 404

so the card shows a text placeholder. The filename path is taken whenever the
disc was not probed -- probeDiscInfo answers "{}" while a game is loaded. Once
that has happened the entry cannot recover: the cached serial is re-seeded into
discInfoCache at the start of every scan and comes back as disc.titleId, which
has top priority.

Normalise the resolved serial rather than the parser, which is what repairs the
already-cached entries since they arrive through the same expression.

Three things this has to get right beyond the cover itself.

NOT EVERY 4+5 TOKEN IS A TITLE ID. FilenameParser takes the first four-letter
plus five-digit token it finds anywhere in the name, so what arrives may be a
release tag or an id belonging to a different game. Left hyphenated a bad guess
matches nothing and the card shows a placeholder -- visibly wrong, and safe.
Stripped, it would become a WELL-FORMED id and quietly resolve whatever is filed
under it: another game's cover, its curated name, and its config_db entry, which
the core applies at boot. So normalise only what carries a real PS3 prefix, B
for disc releases and N for PSN. Deliberately not gated on GamePlatform: that
enum comes from the same probe that produced the serial, so in the one case this
exists for -- probe failed, name came off the filename -- it is always null and
the guard would be constant-true.

THE SERIAL IS NOT ONLY THE COVER KEY. It also keys config.game.<serial>, per-game
core overrides, touch layouts and profiles, pad bindings, play time, the pinned
name and the custom cover file. Renaming the game without moving those resets
every one of them silently, and nothing prunes the old keys, so they become
unreachable rather than merely unused -- the custom cover worst of all, since
CustomCovers.remove resolves through the same name and cannot delete the orphan.
migrateSerialKeys moves them, and CustomCovers.renameSerial follows the file.

THE REPAIR HAS TO REACH EXISTING INSTALLS. cacheKey embeds ScanSchemaVersion, and
HomeViewModel only schedules a scan when that key changes. Without a bump an
upgraded install keeps serving the cached hyphenated ids and never rescans, so
the covers stay broken until the user finds the refresh button. Bumped 7 -> 8;
the constant's own contract asks for this whenever a stored field changes, and a
changed VALUE has the same staleness signature as a new field.

Verified on device, 14-game library with three affected ISOs. Seeded the broken
state (hyphenated serial in the cache, a pinned name and play time under the old
id, cached key at v7), then launched WITHOUT touching the UI:

  load(first): cachedKey=v7|...  newKey=v8|...  pending=true
  scan start: 1 dir(s), rawStorage=true
  serial 'BLUS-30917' -> 'BLUS30917' (3 pref key(s) moved)

pending=true is the field that read false before the bump. Afterwards no
hyphenated key or serial remained anywhere in the preferences, the pinned name
was live on the card under the new id, and Lollipop Chainsaw, Ratchet & Clank:
Full Frontal Assault and Virtua Tennis 4 all render their covers.

Not fixed here: those discs still have no extracted ICON0.PNG, so their offline
fallback stays missing, and the re-probe that would create one is folder-only --
re-probing an ISO needs a vfs::mount, which the seeding loop deliberately avoids.
Two library entries that resolve to the same id can also cross-write each other's
per-serial data; that is the intended merge for a genuine duplicate, but nothing
models it.
2026-08-17 07:51:59 -05:00
Zulux91 89c6d08ed3 cellAudio: don't let a silent port reset the untouched baseline every period
A game can leave an audio port started and write nothing but zeros into it.
Those writes still land on the tag slots, overwriting the -0.0f tag with
+0.0f, and count_port_buffer_tags() detects that sign flip as "the buffer was
touched" -- correctly, since it cannot tell silence from data.

The result is a port that reports untouched on most periods and touched on the
few that a write happens to land in. Storing untouched_expected as the
instantaneous count then drops it to 0 on exactly those periods, so on the
next period the same silent port looks like a newly untouched buffer, and the
loop waits out the whole untouched timeout for it. Every time it flickers.

untouched_expected is now a high-water mark, clamped to active_ports so a port
going away lowers it again.

Measured on device, Tom Clancy's H.A.W.X. 2 (BLES00928), main menu, stock
audio settings (time stretching off, buffer 34), with a temporary probe in the
period loop counting branch hits per second. Same scene, same build, only this
change differing:

                       before      after
  wait_untouched         669          0     hits/s (1000us each)
  MIX                     65        188     hits/s
  advance (forced)        37          0     hits/s
  enqueued_buffers         0        5-7
  untouched > expected   743          0     per second
  untouched_expected     0 in 799   1 in 376  of the second's samples

The port itself is unchanged by this: it is still started, still counted as
active, still mixed. A full-block scan of it reads 0 non-zero floats out of
512 on every one of 875 consecutive periods, which is what makes it silent,
and it is the tag flicker rather than the silence that caused the stall.

Audible effect: the audio clock ran at ~55% of real time (103 vs 189 periods
per second) with the ring buffer permanently empty, which is why the whole
title sounded slowed down and stuttering. Note this happens with time
stretching disabled -- the frequency ratio stayed at 1.000 throughout, so the
slowdown is the period rate itself and not resampling.

Not verified: whether any title depends on untouched_expected falling back to
a lower value within a stable port configuration. Nothing in the tree tests
this loop.
2026-08-17 04:26:09 -05:00
Zulux91 7811cffeed Canary patches: a revision bump must not re-enable the older ones
ensureBundledPatches iterated the whole BUNDLED list and forced each entry
enabled, so bumping BUNDLED_REVISION for one game re-enabled every bundled
patch -- including Sonic '06's Graphics Fix, for a user who had deliberately
turned it off and may not own the game the bump was made for. That directly
contradicts the guarantee written above the function ("turning one OFF
sticks").

It cannot be fixed by reading the state back: save_config writes an entry
only when it is enabled, so "disabled" is stored as an absent entry and
patch_config.yml cannot distinguish opted out from never seen.

Each Bundled entry now records the revision it first shipped in, and only
entries newer than the stored revision are touched. An install already at
revision 1 has been offered the Sonic patch once; whatever the user did with
the toggle afterwards is their answer.

Two things fall out of the same change:

- The retry on partial failure now covers only the pending entries, so a
  patch stuck failing can no longer drag the already-settled ones back on
  with it every boot.
- When nothing is pending the import is skipped entirely rather than
  rewriting patches/patch.yml for no reason, which keeps a future bump that
  adds no new patch from touching the file at all.

Verified on device (arm64, Android 15) against the shape this changes, which
is the upgrade in place: stored revision 1, a populated patch.yml without the
new entry, and no patch_config.yml -- the state a user is in after turning
the Sonic patch off, since disabled is stored as absence.

Booting a game logged

  canary patches: imported 1, enabled 1

("enabled 2" is what the previous code would report, since it enabled
BUNDLED.size entries), and the resulting patch_config.yml contained only

  SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1:
    Bink overlay skip:
      Tom Clancy's H.A.W.X. 2:
        BLES00928:
          All:
            Enabled: true

with no Graphics Fix entry, i.e. the opted-out patch stayed off across the
bump. The Sonic entry in patch.yml itself was preserved, and the stored
revision advanced to 2.

Not verified: the two-writer race on patch.yml (a Patches-tab download
overlapping a boot). That is unaffected by this change and still unguarded.
2026-08-17 03:20:59 -05:00
Zulux91 20c854aeb3 patch_engine: stop save_patches from destroying the file it rewrites
Two defects on the same write path, both reachable today from Download
database and from a local patch import.

1. The file was opened with fs::rewrite (write + create + trunc), streamed
   into, and the write result discarded -- save_patches returned true
   unconditionally. A write that fails part way (out of space, process
   killed) therefore leaves a truncated patch.yml behind, and load() rejects
   the whole file on a parse error, so the failure costs the user every patch
   they had. There is no way to rebuild it from inside the app either:
   import_patches refuses to write when load() fails, so both import paths
   return -1 from then on.

   save_config, 70 lines up in the same file, already writes through
   fs::pending_file and checks the result. save_patches now does the same.

2. The address element was always emitted as fmt::format("0x%.8x", offset).
   For move_file and hide_file that element is a VFS path, not a number:
   load() keeps the text in original_offset and skips the u32 validation for
   those two types. So a round trip turned a path into 0x00000000, and the
   loader accepted it back -- the patch still lists and still toggles, it just
   silently stops matching anything. Re-downloading does not repair it,
   because append_patches discards an incoming patch whose Patch Version is
   not strictly greater than the stored one.

   The emit is now gated on patch_type_uses_hex_offset, the predicate that
   already existed for this and was used only on the load side.

   The numeric branch deliberately keeps using offset rather than
   original_offset: an address modifier is folded into offset at load time,
   and the flat form emitted here has nowhere to put it.

Both predate the Android patch work and apply to upstream RPCS3 unchanged;
they are in this branch because the bundled-patch import adds another caller
of save_patches.

Verified on device (arm64, Android 15). A patch.yml seeded with move_file and
hide_file entries was put through an import that merges a new patch, which is
what forces the rewrite. After it:

  - [move_file, /dev_bdvd/PS3_GAME/USRDIR/probe.bik, /dev_bdvd/PS3_GAME/USRDIR/probe.bik.bak]
  - [hide_file, /dev_bdvd/PS3_GAME/USRDIR/hidden.bik, ""]

Both paths survived; before this change they would read 0x00000000. All three
top-level hashes in the file (the two seeded, plus the merged one) were still
present and parseable afterwards.

Not verified: the failure path in (1). Forcing a short write mid-rewrite
(ENOSPC or a kill inside save_patches) was not exercised, so the atomicity is
argued from fs::pending_file's contract and from parity with save_config, not
from a reproduced failure.
2026-08-17 03:20:43 -05:00
Zulux91 7952244052 Stop two guest polling loops from flooding the log
cellMicOpenEx logged at notice and sys_net_bnet_accept at warning, once
per call. Titles poll both. In H.A.W.X. 2 they are called roughly 100 and
200 times a second respectively for the whole session, and together they
were 46% of the log -- 27305 lines of 58441, about 9 MB per three minutes.

On Android that file is on FUSE-backed storage, where writes are far
slower than the f2fs the emulator's own data sits on, so this is not just
noise in a text file.

Neither call is an error. cellMicOpen and cellMicOpenRaw are thin wrappers
around cellMicOpenEx and were already trace, so the wrappers were quieter
than the function they call. A non-blocking accept() on an idle listening
socket is a normal polling pattern, not a warning.

After: 4 and 1 lines respectively, log down to 2.7 MB over the same span.

Also corrects the heap-flag test in mem_allocator_vma: the loop checks a
VkMemoryHeap::flags value against VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
which is a memory-type property rather than a heap flag. Both constants
are 0x1 so behaviour is unchanged; this only puts the right enum on the
test.
2026-08-17 02:20:34 -05:00
Zulux91 4179f23e20 Bundle the H.A.W.X. 2 Bink overlay patch and enable it
Tom Clancy's H.A.W.X. 2 (BLES00928) hangs forever at the first intro
video. The SPU dies with "Access violation reading location 0x20" in
CellSpursKernel0 and is parked with dbg_pause, which nothing in the
Android build can clear, so the emulator sits at a locked 30 fps while
the guest is dead. Upstream RPCS3 lists the title as Loadable with no
fix but "delete data/movies".

The title looks up a section named '.reload' in an SPU module embedded
in its own EBOOT. That module is stripped -- e_shnum is 0 -- so the
lookup cannot succeed on hardware either, and the game copes: the
failure path writes 0 to the work descriptor's +0x10 field, and the same
module tests that field to skip the overlay load.

  03224  lqr r8,0x1b810     ; r8 = desc[+0x10]
  0322c  brz r8,0x32cc      ; == 0 -> skip

A bump allocator on the PPU side then runs over that field
unconditionally -- (0 - 0x10) & ~0xF = 0xfffffff0 -- destroying the
sentinel. The guard stops firing, so the SPU issues GET lsa=0 ea=0
size=0x4000, a transfer that would have overwritten the running SPURS
kernel had it succeeded.

The patch makes the overlay routine at LS 0x3208 return immediately,
which is what the surviving guard would have caused anyway. It is keyed
on the SPU image hash, so it cannot affect another title or a build of
this module that does carry sections.

Suppressing the DMA emulator-side instead does not work: the guest loop
waits on data that never arrives and runs away into a second fault. So
does zero-filling local store, which breaks the SPURS kernel's own HALT
assertion earlier than the fault it was meant to prevent.

Verified on device with every diagnostic reverted and default settings
(PPU/SPU Recompiler (LLVM), Accurate SPU DMA off): the import runs at
boot, patch.yml grows 468 -> 761 bytes with the existing Sonic entry
preserved, patch_config.yml enables both, and the core reports

  PAT: Applied patch (hash='SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1',
       description='Bink overlay skip', ...)
  ppu_loader: SPU executable hash: SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1 (<- 1)

0 access violations, intro cinematic plays, title screen reachable and
the first mission's targeting-pod sequence renders.

BUNDLED_REVISION goes to 2 so existing installs re-import.
2026-08-17 00:31:29 -05:00
Ani 3be5aa99cc gui: Correctly disable the View Folder button 2026-08-17 02:22:11 +02:00
Zion Nimchuk 3e68a7f385 Add 60 second retry to translation downloads in CI to avoid rate limits 2026-08-17 01:20:35 +02:00
kd-11 9a4b849260 rsx: Fix fbo offset scaling when src and dst bpp is mismatched 2026-08-17 00:15:54 +03:00
kd-11 9a24c8d11f gl/vk: Fix flattened interpreter input subresource range computation 2026-08-17 00:15:54 +03:00
kd-11 0617eff348 rsx: Ensure ref_address is properly set for all sampled images 2026-08-17 00:15:54 +03:00
kd-11 003b368980 rsx: Drill down the copy specification from descriptors when handling dynamic copies 2026-08-17 00:15:54 +03:00
kd-11 6f5f198acd rsx: Fix check for cyclic ref in fast_fbo_check 2026-08-17 00:15:54 +03:00
kd-11 107b751a43 rsx: Enable fast path when scanning for 3D mipmaps 2026-08-17 00:15:54 +03:00
kd-11 4475671bbf RSX: Allow process_framebuffer_resource_fast to take in descriptors with an offset
- Allows to skip going through the merge route when we already have a good match
2026-08-17 00:15:54 +03:00
kd-11 7e35f59997 RSX: Implement host-side mipmap scanning for 3D textures 2026-08-17 00:15:54 +03:00
kd-11 970d745818 rsx: Respect mip levels actually used during image reconstruction 2026-08-17 00:15:54 +03:00
kd-11 adec3ae7f9 rsx: Implement per-mip-level size calculation logic and use it to properly compute 3D texture slice height 2026-08-17 00:15:54 +03:00
kd-11 5e2d0eb762 rsx: Fix incorrect calculation of texture size when border texels are present
- The computation did not match get_subresources_layout behavior.
2026-08-17 00:15:54 +03:00
kd-11 26782525f4 rsx: Fix get_texture_size for 3D textures with mipmaps
- Depth also shrinks for every mip level
2026-08-17 00:15:54 +03:00
kd-11 d0e6d4eefc rsx: Minor improvements to texture cache
- Adds depth to temp subresource key to avoid 3D mismatch (resample)
- Fix narrowing warning for block_h calculations
2026-08-17 00:15:54 +03:00
kd-11 f19d398bec rsx: Check for completeness in X when gathering slices
- This was intentionally ommitted as a speedhack before, but makes sense to check just in case.
2026-08-17 00:15:54 +03:00
kd-11 6b80ac4805 rsx: Simplify merged source sorting when selecting slices
- Sort ranges is a relic and the information is duplicated in the sort_list object
2026-08-17 00:15:54 +03:00
kd-11 13ceef9f46 rsx: Fix slice gather from local resource (e.g blit engine output) 2026-08-17 00:15:54 +03:00
Walter ffc50905a6 [SPU LLVM] Avoid GFNI combine bug in SHUFB
Due to a bug where `SHUFB`'s GFNI constant generation path expects to be combine using a select instead of a OR, it was causing issues with on non-AVX512 CPUs so support was reverted (see #19217). That can still happen on AVX512 CPUs when the shuffle is single source. This patch fixes it and re-lower the target requirements back to just GFNI by avoiding the OR well on the GFNI path. I also renamed a variable and added a comment to better clarify its behavior.
2026-08-16 19:35:42 +03:00
jpolo1224 4b2b8438be Merge PR #50: UI: Lingering Playstation 2 naming on the side menu during execution 2026-08-16 10:43:08 -04:00
jpolo1224 7b49e1fcea Merge PR #55: Android CPU time: park the dma_manager::sync() wait, and stop re-parsing the global config every 5s 2026-08-16 10:43:08 -04:00
jpolo1224 d9957c56ae Merge PR #57: SPU: make the ARM64 uncompilable-block fallback safe to enter 2026-08-16 10:43:07 -04:00
jpolo1224 62d8208c71 VK: frame generation's Motion detail slider was inverted
framegen treats flowScale as a DIVISOR -- flowExtent = inputExtent / flowScale in
v3.1_src/shaders/mipmaps.cpp -- which is why upstream's own layer passes
1.0f / conf.flowScale rather than the value itself.

We passed value / 100 from a 25..100 setting, so every position below the default
asked for a LARGER optical-flow pyramid instead of a smaller one:

  100 -> 1.00 -> full resolution          (correct, 1.0 being its own reciprocal)
   64 -> 0.64 -> 1.56x per axis, 2.4x px
   25 -> 0.25 -> 4x per axis,   16x px

So a user turning "Motion detail" down to find speed got sixteen times the flow
cost at the bottom of the range, and the slider got slower the further it was
turned down. Only the default was ever right, which is why this survived testing.

Now 100 / value. The ~10% of real framerate frame generation already costs is not
this: that was measured before the setting existed, when the call site passed a
hardcoded 1.0f. Anything measured since, at a non-default value, was carrying the
inflated cost.
2026-08-16 10:37:46 -04:00
Zulux91 cab4f2507c SPU: make the uncompilable-block fallback safe to enter
The failed-block set is consulted by two lookups that locate a candidate with
upper_bound and step back exactly one entry, so they only ever examine a single
range. That is correct only while no range can hide another, and nothing kept
the set disjoint. The two marking call sites record different extents: one
records a whole analysed program, the other records an entry point alone when
there is no program to describe. An entry-only mark landing inside a
program-sized mark is therefore ordinary, and it always ends first, which leaves
the enclosing range invisible for every address past its end.

mark() now merges on insert, so the invariant the cheap lookup depends on holds
by construction. The set moves into spu_failed_block_set (SPUFailedBlocks.h),
header-only and free of engine dependencies so it can be exercised directly
rather than through a model of it.

A hole was not merely a missed optimisation. dispatch armed the fallback with
whatever the lookup returned, and old_interpreter releases the thread when
(pc < begin || pc >= end), which is unconditionally true for an empty range, so
the interpreter would return having executed nothing while dispatch re-entered
at an unchanged pc. spu_arm_interp_fallback now yields a range that contains pc
and is non-empty, recording the block first when no path had recorded it. It
does that under one critical section rather than lookup, unlock, mark, look up
again: nothing removes ranges concurrently today, so the gap was not live, but
the guarantee rested on who happens to call the reset rather than on structure.

It also recorded only [pc, pc + 4) while dispatch was holding the analysed
program, so the interpreter released the thread after a single instruction and
dispatch re-entered four bytes later to pay another full analyse and another
full failed compile -- the 4-bytes-at-a-time walk documented at the top of this
file. The extent is passed through when the caller has one. That path no longer
logs "cannot be compiled on this backend" either: a null compile with no
diagnostic also covers a poisoned engine, an analyser that produced nothing for
a branch into data, and a lost compile claim, none of which are backend limits.

The interpreter also ran in the wrong place. It was started from
spu_thread::cpu_task after dispatch had escaped, which executes guest code
outside any gateway invocation, while spu_runtime::g_escape resumes through the
gateway epilogue whose address and stack pointer the prologue stored in hv_ctx
-- belonging to a call that has already returned. A guest HALT, an MFC interrupt
or cpu_work escaping from inside the interpreter would restore a stack pointer
into a dead frame. It now runs from dispatch, inside the live gateway call.

allow_interrupts_in_cpu_work is not restored after the old_interpreter call,
because an escape out of the interpreter is a far jump to the gateway epilogue
that abandons every frame in between -- a restore placed there is skipped on
exactly the paths the flag is set for. Both that flag and interp_fallback are
cleared by cpu_task before each gateway entry instead, which is the one point
every escape returns through. interp_fallback was previously left set when
old_interpreter exited through check_state() as well.

spu_interpreter_fallback_available() tested spu_runtime::g_interpreter, the
LLVM-built interpreter used when a recompiler is selected. The fallback actually
run is old_interpreter, which reads the opcode table, the thread and the local
store and nothing else. When the LLVM interpreter failed to build, that check
disabled a fallback which was in fact available and dispatch took the
"Compilation failed" path instead.

The set is now also cleared per emulation session. Its keys are local-store
offsets, which every SPU thread, every image and every title in the process
reuse, so a set that outlived the session let one title's compile failures route
an unrelated title's code at the same offset to the interpreter. The call is
guarded by ARCH_ARM64: the set and its accessors exist only on that backend,
which is the one that can fail to compile a block.

tests/test_spu_failed_blocks.cpp covers both hole shapes, the half-open
boundaries, the merge cases in both orders and the local-store extremes. Its
load-bearing case is MatchesReferenceCoverage, a randomized differential against
an independent bitmap, which constrains the union, the maximality of range_of
and the "coverage grew" return value together for sequences nobody chose by
hand. is_disjoint() has no reachable negative through the public API and is
documented as a witness rather than presented as a check. The file also names
the runtime paths it cannot reach. It is registered in rpcs3_test.vcxproj as
well as the CMake list; the Windows CI job runs the MSVC build, where it would
otherwise have been absent while reporting green.

Executed. The ARM64 core builds clean, no warnings. The interval set passes a
randomized differential run directly on the header (200 trials, 4800 mark
operations, 0 mismatches); the pre-merge algorithm fails the same oracle 1268
times. On device (Snapdragon 8 Elite-class, Android 15), a throwaway build that
forces compile failures drove the fallback end to end for the first time: a
block with no prior mark recorded its whole 680-byte analysed extent in one
mark, and a pre-marked block returned its covering range; both were interpreted
inside the live gateway frame and escaped, with Mirror's Edge holding its title
screen at 30.00 fps and Metal Gear Rising at 457 present frames over 8m44s with
no "Compilation failed".

Not executed. No x86-64 build and no rpcs3_test binary: the header's evidence
comes from a standalone host harness and mutation runs, not from the registered
gtest, and the ARCH_ARM64 guard on the session reset is unverified by
compilation. The dispatch re-entry fast path recorded zero hits in every device
leg, so the exposure from merging ranges -- previously-JIT'd addresses routed to
the interpreter for the rest of the session -- is unmeasured. The same forced
failure applied to the pre-change code did not fail on device either, so these
runs show the new path is correct and free, not that it is necessary; the escape
from a dead gateway frame needs a HALT, an MFC interrupt or cpu_work to fire
while inside the interpreter, which one short interpreted block did not reach.
2026-08-16 06:10:31 -05:00
kd-11 cbc7b60ba5 rsx: Cleanup 2026-08-16 13:50:03 +03:00
kd-11 47efd770a8 rsx: Clean up get_merged_texture_memory_region
- Cleaner generation of src_area and dst_area outputs.
- Normalized comparisons in 1bpp space then convert to target space after.
2026-08-16 13:50:03 +03:00
kd-11 f53547b173 rsx: Refactor deferred_subresource constructor into discrete wrappers for each output intent
- Instead of filling over 10 arguments and having the ctor silently drop half of them, we create proper wrappers to construct objects for a singular purpose.
2026-08-16 13:50:03 +03:00
kd-11 cb5b866c61 rsx: Refactor deferred_subresource to be more explicit
- Use defined src and dst rects as well as the transformation if any to be applied.
2026-08-16 13:50:03 +03:00
Zion Nimchuk 2f3c0f04d2 Update docker with updated SDL3 2026-08-16 09:57:33 +02:00
schm1dtmac 3f493fb209 [Qt] Hide titlebars by default 2026-08-16 02:07:48 +02:00
Antonino Di Guardo f7eb0d8d76 Enrich game list title (#19229)
Add on Game List title a brief recap of total number of entries in the
list and total number of Disc, HDD and all the other remaining types of
content.
2026-08-15 23:19:01 +00:00
Zulux91 27e9d11b80 UI: stop re-parsing the whole global config every 5 seconds of gameplay
ConfigStore.loadGlobal() is a JSON parse plus every migration block in the
file -- 24,555 dex instructions by ART's own count, over its JIT ceiling,
so it runs interpreted on every call. EmulationSurface's frame-rate
monitor calls it (via resolveForGame) every 5 seconds for the whole
session, to read a single boolean. Measured: one ART "exceeds compiler
instruction limit" bailout line per 5.00 s of gameplay, entire sessions
long.

Memoize the parsed Settings. The migrations are one-shot behind their own
prefs flags, so caching is behavior-identical; saveGlobal is the only
writer of the pref after boot and refreshes the cache, and
reconcileReusedFolder -- whose restore path writes the pref directly,
before any settings screen exists -- drops it.

Measured after, same scene: one bailout line for the whole session (the
single first-call compile attempt) versus twelve per minute before, and
the boot config dump still carries the user's settings.
2026-08-15 08:17:33 -05:00
Zulux91 6e731093c4 RSX: park the dma_manager::sync() wait instead of spinning
The RSX-thread branch of dma_manager::sync() busy-waited on the offloader
with a pure pause() loop. Measured on Metal Gear Rising gameplay (Odin,
warm shader cache, off-CPU profile): the loop held 26.6% of the RSX
thread's wall time while the RSX Offloader thread itself was parked in a
kernel wait for 99.78% of the same window -- the spin was paying the
offloader's wake-up latency on every small handoff, burning about a
quarter of a core to wait for a mostly-idle thread.

Spin briefly for the short common case, then wait on m_processed_count
with a 100us timeout. The offloader notify_all()s that atomic when its
queue drains; the timeout is load-bearing, not a formality -- an
offloader stopped mid-job by a memory fault cannot notify (it spins in
on_access_violation until this thread's upkeep clears the deadlock
flag), and the upkeep can itself enqueue new jobs from inside the wait,
deferring the equal-counters notify to the next drain.
on_semaphore_acquire_wait() still runs every iteration.

Three refinements from an eight-pass adversarial review of the first
version of this change:

- The wait targets the processed count the loop condition observed, and
  parks only if a re-read after the upkeep call shows no progress. The
  drain-notify is one-shot: parking on a pre-upkeep value absorbs a
  full timeout when the offloader drained during the upkeep, and
  parking on a blind re-read turns any partial progress into an
  immediate return, degrading the park into a hot upkeep loop for the
  whole drain.
- If the offloader thread is not running (config toggled on mid-session
  after booting with it off, aborting, or dead from an unrecoverable
  fault), the drain can never come; keep the visible spin there so the
  pre-existing hang stays attributable in a profiler instead of
  presenting as an idle, healthy-looking app.
- The comment states the timeout's real role; the first version claimed
  nothing else could enqueue during the wait, which is false (the
  upkeep's flush path reaches backend_ctrl) and would have licensed
  removing the timeout.

Measured after (same scene and script, healthy device): sync() falls to
0.10% of the RSX thread's wall time, the thread parks in the kernel for
67.6% of the workload, and fps is unchanged within run noise (52.9 avg
vs 51.4 for the pre-review variant in the same session). The win is a
freed core and its thermal budget, not frame rate. The
non-RSX-thread branch has the same spin shape; it was not measured and
is left untouched.
2026-08-15 06:47:59 -05:00
digant73 fc93d932c8 Swap PR number with PR text in update manager 2026-08-15 13:28:58 +02:00
Diego BM 89b2128679 Update EmulationMenuScreen.kt
Lingering Playstation 2 naming on the side menu during execution
2026-08-15 13:14:33 +02:00
jpolo1224 33343cd153 Release 0.8 2026-08-15 01:26:54 -04:00
jpolo1224 6cd7866553 i18n: Brazilian Portuguese corrections
From johnpetersa19. Fixes renderer.shaderChain.pass, which read "passar" -- the
verb "to pass" rather than a rendering pass -- and its plural, translates a
label left in English, and adds the packages.* strings added after the original
translation. Also drops five strings that were stored truncated mid-sentence;
English is better than half a sentence.
2026-08-15 01:26:54 -04:00
jpolo1224 6a5ad70ec7 Updater: pick the release asset that matches this build
A release now carries four APKs rather than one, so the updater has to choose
the asset built for the device it is running on instead of taking the first it
finds. Matches on the variant suffix in the asset name and falls through to the
next release rather than giving up when one has no usable asset.
2026-08-15 01:26:54 -04:00
jpolo1224 c990f34d5f UI: frame generation controls, and route the setting to the core at all
Adds the import row for Lossless.dll, the multiplier, Performance shaders and
Motion detail, plus the strings for all of it.

Frame Generation was not reaching the emulator. Rpcs3Bridge.setSetting is a
translation table keyed by (section, key) and anything absent is silently
dropped, so the toggle looked like it worked and did nothing. Enums also have
to cross as NAMES rather than indices -- sending "1" would have been wrong even
with the entry present. Found by an unconditional probe in the present path,
after being wrong about the cause twice; the probe printed mode=0 while the UI
held 1, which was the whole answer.

Performance shaders default ON. It selects framegen's 3.1p shader family
instead of 3.1, which is materially cheaper, and on a mobile GPU the
full-quality path costs more than the frames it buys. Both families are
extracted from the user's DLL already, so this switches between shaders that
are both sitting in the cache.

Motion detail is the optical-flow resolution, stored as a percentage rather
than upstream's divisor so the slider reads the right way round. Both take
effect when frame generation next starts, since the shader family and the flow
scale are baked into framegen's device and pipelines at initialize; the
descriptions say so.

The description also warns about the two things testers will otherwise report
as bugs: on-screen text shimmers because the overlay and the game's own menus
are interpolated along with everything else, and toggling mid-game pauses for
a few seconds while a second device and the pipelines are built.
2026-08-15 01:26:44 -04:00
jpolo1224 bbaebe47a4 UI: make the OSD colour control change the OSD, and let the position move in game
The "OSD Color" row -- on the Overlay tab and cycled from the in-game menu --
wrote `osdColor`, which is PCSX2's EmuCore/GS/OsdColor plus a
NativeApp.osdSetColor() that is an Unsupported.note() stub here. Both dead, so
the control had never done anything and the overlay sat on whatever RPCS3
defaulted to, while the real picker sat a hundred lines further down the same
tab. Both rows now drive ps3.overlayBodyColor.

The defaults were also wrong in a way that made this worse: they held RPCS3's
RGBA hex verbatim in fields that argbToRgba reads as ARGB, so every channel was
rotated one byte and #FFE138FF orange rendered as #E138FFFF. That is the pink
the overlay has always drawn in, and it applied to picked colours too, so
nothing ever matched what the user chose.

The preset row shows no selection when the colour came from the RGBA sliders,
and the in-game row reads "Custom", rather than naming a preset that is not
active. Overlay position is now cycled from the in-game menu as well -- it was
only in All Settings, unreachable at the one moment it matters, when the stats
are sitting on top of something you are trying to see.

Also carries the two frame generation settings fields, which live in the same
Ps3 settings class.
2026-08-15 01:26:29 -04:00
jpolo1224 e480c291da Emu: say more when a thread dies, and less when a game polls
The one-shot PPU state dump now follows its summary with what each PPU can
report about itself -- registers, the guest call stack, and the recent guest and
HLE/LV2 calls when PPU Calling History is on. Diagnosing the Saint Seiya stall
meant reconstructing that by hand from a log that only named the thread; cia
under the recompiler is written at block boundaries, so it names where a thread
has BEEN, not where it is, and the call history is only populated by the
interpreter.

cellSysutil's parameter query drops from warning to trace. Eternal Sonata
(BLJS10017) asks for ID_ENTER_BUTTON_ASSIGN twice every 33 ms and never stops,
which is about sixty lines a second for an entire session. Games polling this
is normal behaviour, not something to warn about, and the log volume alone is
enough to slow the emulator down.
2026-08-15 01:26:16 -04:00
jpolo1224 7d25a7086e VK: frame generation through Lossless Scaling, experimental
Interpolates frames between the ones the game draws, at x2/x3/x4. The shaders
come from the user's own Lossless.dll; nothing is bundled or downloaded.

framegen runs on its OWN VkDevice and statically links volk, which defines 655
globals named vkCreateImage, vkQueueSubmit and so on -- including all 124 our
loader declares. Linked into the core those either fail to link or, worse,
merge, and framegen's volkLoadDevice() then repoints the whole RSX renderer at
framegen's device. So it lives in libarmsx3_lsfg.so, reached only by dlopen
with RTLD_LOCAL, behind a C ABI and a version script that exports eleven
symbols and nothing else. Verify with llvm-nm --dynamic --defined-only: only
armsx3_lsfg_* may appear.

Two devices with no shared semaphore means images cross as AHardwareBuffer --
Adreno and Mali both refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory,
so upstream's FD path does not work on this hardware. Capture costs 0.007
ms/frame CPU, measured; the cost is the synchronisation, not the copies.

Notes for anyone reading this later:

  * The shader loader's user pointer must outlive initialize(). framegen copies
    the callback into ShaderPool::source and resolves shaders lazily while
    BUILDING THE CONTEXT, so a stack local there is read back from a dead frame
    -- a segfault executing at a mapped, non-executable address.
  * The "device UUID" is not one. framegen matches (vendorID << 32) | deviceID.
    Zero matches nothing.
  * Imported shaders are cached to disk. They used to live only in the library's
    map, so every restart silently had none and generate() returned 0 before
    doing any work.
  * Capture takes the COMPOSITED swapchain image, after overlays. Capturing the
    game image put the perf overlay on real frames only, so it blinked at half
    the display rate.
  * generate() runs only on a frame the game actually drew, or the PPU/SPU
    compilation screen gets interpolated too.

The pipelined path that would take waitIdle off the critical path is present but
disabled behind k_framegen_pipelining_enabled: holding a frame back conflicts
with frame-context recycling, and at least one reclaim path has not been found.
The serialised path is what works. Frame generation costs some real framerate
and wants a steady one -- interpolating an unstable rate reads as judder -- so
it is labelled experimental in the UI.
2026-08-15 01:25:21 -04:00
jpolo1224 dbbb6fbde0 VK: use extended dynamic state to collapse pipeline permutations
Cull mode, front face, depth test/write/compare and primitive topology move out
of pipeline identity and into per-draw state where VK_EXT_extended_dynamic_state
is available. Fewer pipeline objects to compile and cache is worth a lot on
Adreno and Mali, where first-run compilation is a visible source of stutter.

Topology only collapses within its class -- triangle list/strip/fan share one
pipeline, lines share one, points stand alone. vkCmdSetPrimitiveTopology cannot
cross classes without dynamicPrimitiveTopologyUnrestricted, which comes from
extended_dynamic_state3 and is not something mobile drivers report. The class
representative is restart-aware: primitive restart on a *_LIST topology is
illegal without primitiveTopologyListRestart, so a restarting draw is
represented by the strip form or pipelines that build today start failing
validation.

Gated on the feature bit, not the extension string, and enabled at device
creation; without it the props keep their real values and the command stream is
byte-identical to before. Entry points go through the existing VKProcTable
wrangler, so vk_android_loader needs no regeneration.

pipeline_props keeps its shape: the disk cache stores it as a raw struct, so
the VALUES are normalized before it is used as a key rather than teaching
operator== about the extension. The shader cache directory becomes v1.96-eds
against v1.96 -- the suffix matters because support depends on the DEVICE, and
a driver can be swapped in through adrenotools between two runs of the same
game. Reading a normalized entry back without the extension would silently
build pipelines with culling off and depth compare NEVER.

Depth bounds, stencil, and the EDS2/EDS3 states stay static: depth bounds is
constant per device and never differentiated anything, and stencil is already
all-zero for the overwhelming majority of draws.
2026-08-15 01:25:01 -04:00
jpolo1224 d069a55acc VK: a lost surface is recoverable, not fatal
Leaving the app during a game aborted the process outright:

  Assertion Failed! Vulkan API call failed with unrecoverable error:
  Surface lost (VK_ERROR_SURFACE_LOST)   swapchain.cpp, swapchain_WSI::init()

Losing the surface is routine on Android -- the ANativeWindow is destroyed
every time the app leaves the foreground -- and the renderer already treats it
as recoverable everywhere else, setting m_surface_lost in both the acquire and
the present paths. Only swapchain init went through die_with_error.

All three surface queries in init() now return false instead of aborting, and
record which kind of failure it was. The caller needs that distinction: "the
window is minimized, retry later" and "the VkSurfaceKHR is dead" both surface
as a false return, but retrying against a dead surface queries the same dead
handle forever. Only the second recreates the surface first.

That also removes the memory corruption behind it. The fatal error killed the
RSX thread mid-operation and the Main Callbacks thread then destroyed its
objects, so tearing down ZCULL state freed a container that was still being
written -- scudo reportInvalidChunkState inside ~ZCULL_control. No fatal
teardown, no corrupted teardown.

~ZCULL_control is tightened regardless: it now drains page refs and resets prot
the way unlock_pages does, rather than freeing pages that still hold references
and leaving m_critical_reports_in_flight unbalanced -- harmless at process exit,
wrong on a restart within the same process, which is every restart here. Note
its m_pages_mutex is the only place that lock is taken; every real writer is
externally synchronized and locks nothing, so holding it must not be mistaken
for protection against a writer that is still running.
2026-08-15 01:24:47 -04:00
jpolo1224 614bf8b718 Android: re-deliver the Surface, so a missed one cannot strand the renderer
Opening a game the instant the app started left a black game area forever,
while rotating the device "fixed" it. SurfaceHolder.Callback::surfaceChanged is
a one-shot -- Android delivers it when the surface is created or resized and
never repeats -- and getNativeWindow() blocks until that single delivery
arrives, in a 100 ms sleep loop with no timeout. One missed delivery therefore
parks the RSX thread for the rest of the session. A rotation only helped
because a configuration change forces a fresh surfaceChanged.

EmulationSurface now re-delivers holder.surface on attach and on window
visibility changes. It is idempotent: the native side compares the incoming
ANativeWindow against the one it holds and no-ops on a match, so this costs
nothing when the first delivery already arrived. It has to be post()ed, since
onAttachedToWindow runs before layout and a 0x0 report is explicitly ignored.

The wait loop also logs now, every three seconds, because the failure was
otherwise completely silent: the emulator log stopped dead just after Vulkan
device creation, the perf sensor read 0.0% CPU, and nothing said why. Diagnosis
took a screenshot and dumpsys SurfaceFlinger to establish the surface existed.

Adds GSFrameBase::display_epoch, bumped when the native window is replaced. The
swapchain is rebuilt on a size mismatch and nothing else, so a replacement
window at identical dimensions was invisible; platforms that cannot swap a
window under a live swapchain keep the default and are unaffected.
2026-08-15 01:24:33 -04:00
jpolo1224 cce09dbb39 SPU: recover from a failed analysis, and stop the log floods
Three faults that showed up in tester logs, all of which made the emulator
look broken in ways the log then hid.

Eternal Sonata flooded with SPU "Invalid code" errors: when the analyser
produced no data the recompiler had an empty branch with a TODO where the
fallback belonged, so the block was neither compiled nor marked, and the same
address was retried forever. It now marks the block failed and lets the
interpreter take it -- 6320 errors in one session down to none.

The unknown-instruction and halt messages are rate-limited, per opcode and per
address rather than globally, so a repeating fault reports once instead of
every execution. One tester's log went from 600 MB to 2.0 MB; the log volume
itself had been slowing the emulator, so this is not only a readability fix.

ARM64 fault classification in Thread.cpp preferred a heuristic comparing
si_addr against the PC, which misreads a genuine data fault as an instruction
fetch. It now decodes ESR first and only falls back to the heuristic, and an
SPU halt at the 0xffdead00 sentinel is reported as a guest assertion rather
than a host segfault. BLEACH crashed here, and the misclassification gated
every recovery path behind it.
2026-08-15 01:24:19 -04:00
jpolo1224 b5a715adcf PPU: give the AArch64 register scavenger the spill slot it needs
Saint Seiya: Sanctuary Battle (BLES01421) stalled partway through PPU
compilation and booted to a black screen. The failure was in LLVM, not here:
on AArch64 the register scavenger ran out of registers under the GHC calling
convention, which pins most of the GPRs to guest state, and
AArch64FrameLowering::determineCalleeSaves returns early for GHC before it can
create the emergency spill slot the scavenger falls back on. The scavenger then
aborts, and because that takes down the whole MODULE rather than one function,
every function in it drops to the interpreter -- the boot never finishes, or
the game runs at interpreter speed with nothing in the log to explain it.

Fix creates the spill slot for GHC frames that actually need stack. 231/231
modules compile for Saint Seiya, and Sonic Unleashed's FMVs work for the same
reason. Because it is a codegen fix rather than a per-game workaround, any
title that hit this benefits.

The change itself lives in the LLVM submodule, whose remote is upstream
llvm/llvm-project, so it cannot travel in this repository. It is preserved
here as 3rdparty/llvm/armsx3-aarch64-ghc-emergency-spill.patch, applied
against the pinned submodule commit; a build without it applied will exhibit
the original stall.

Also bumps the ARM64 codegen cache version so caches produced before the fix
are not reused, and carries the PPUTranslator changes the same work needed.
2026-08-15 01:24:06 -04:00
jpolo1224 eb54f9b75a Build: four release variants, and what the new one needed
Splits the Android release into legacy / a11 / a13 / a15 so a device can take a
build matched to its CPU and OS instead of one binary suiting everything.
android/build-variants.sh drives all four from a single table of
(ndk, api, -march, apk suffix), and ConfigureCompiler.cmake takes -march per
variant rather than hardcoding one.

The legacy variant had never been compiled before: every release up to 0.7.2
was built at the gradle default of minSdk 33, so nothing had ever targeted a
lower API. Doing so turned up std::aligned_alloc, which is API 28+ -- below
that <cstdlib> does not declare it at all and the using-declaration fails to
resolve. posix_memalign is the older spelling and its result frees with plain
free(), so the rest of the header is unaffected. Kept even though legacy now
targets API 30, because it costs nothing and the next person to try a lower
floor should not rediscover it.

legacy targets armv8.1-a, which is the floor this codebase compiles at rather
than a preference: util/simd.hpp uses SQRDMLAH (v8.1 RDMA) and util/asm.hpp
has inline LSE atomics, so armv8-a does not build. Its value is cores that are
ARMv8.2 without the OPTIONAL fp16 and dotprod extensions the other three
variants require. Cortex-A53/A72/A73 class parts stay out of reach until those
two paths gain fallbacks.
2026-08-15 01:23:53 -04:00
digant73 3cbf9b8b6c fix crash with vfs exception 2026-08-15 04:39:42 +03:00
jpolo1224 0a9fd15b57 Merge branch 'pr41' 2026-08-14 12:21:02 -04:00
Zulux91 0821bbf956 Emu: complete abandoned UE3 HD-cache install at boot (Larry: Box Office Bust)
Leisure Suit Larry: Box Office Bust (BLUS30331) copies its disc asset tree into
an on-HDD cache during a short boot window and abandons the copy when emulated
I/O is slower than a console, then crashes at "New Game" on the missing packages
(upstream RPCS3 #14402). Finish that copy once, at boot, before the guest runs.

complete_ue3_hd_cache() runs in Emulator::Load after the bdvd+hdd0 mounts and
before Run(). It is gated to a verified title-ID allowlist ({BLUS30331}):
PS3TOC.txt is a generic UE3 marker, so keying on it alone would act on other UE3
discs and build the write root from an unvalidated PARAM.SFO TITLE_ID. It parses
the disc PS3TOC.txt manifest, confines each entry textually (rejecting
traversal/drive/UNC/reserved names), copies each not-yet-complete asset
atomically via fs::pending_file, and stamps a 0-byte <file>__time sidecar to the
disc source mtime, mirroring the guest's own completeness convention.
Completeness is keyed on the sidecar AND the dest byte size, so a guest-truncated
payload is re-copied rather than skipped. On any parse/stat/space/copy failure it
returns install_failed after Kill(false), like the sibling post-ready error
exits, so the boot aborts cleanly instead of handing the guest a half-install.

For any other title the function returns after a single title-ID comparison,
before any filesystem access.

Validated on-device (Odin 3, Adreno 830): cold cache -> 800 files / 1847 MiB
copied in ~29s -> New Game reaches the Prologue, 0 access violations; 2nd boot
does no work (idempotent); a forced install_failed tears down cleanly with no
crash; Lollipop Chainsaw and Mirror's Edge boot unaffected (completer inert).
2026-08-14 10:59:34 -05:00
Zulux91 b29810d1a5 RSX: second hardening round for the semaphore wait, from re-review
A blind re-review of the previous commit (six lenses, fresh reviewers)
found real gaps in the hardening itself. Addressed here:

- The EVTSTRM gate failed open to the spin: with the event stream absent
  on a core whose armed WFE does not park, disabling the fallback
  reinstated the original full-rate spin. The paced tier now degrades to
  a 100 us scheduler sleep instead, which also keeps the timeout and
  service polls running at a bounded cadence.

- Gate the FIFO-idle wait_for_event() the same way (three reviewers
  independently flagged the contradiction between asm.hpp's new
  precondition and this ungated sibling). Without the stream it yields,
  which is that path's pre-WFE behavior.

- Non-Linux ARM64 now defaults to the previous commit's behavior instead
  of silently disabling the fallback: the false default was a regression
  against 002a9b274 on the Apple Silicon and Windows-on-ARM targets, and
  no HWCAP equivalent exists there to probe.

- The loop's snapshot is now read through the existing atomic reference
  (relaxed observe()) instead of a plain reference: the previous form was
  a formal data race whose correct codegen depended on an unrelated
  virtual call staying opaque to the optimizer.

- Guard unaligned semaphore addresses on the acquire path: exclusive
  loads fault on unaligned addresses, semaphore_release already rejects
  them, and acquire did not. Unaligned waits now use the paced tier only,
  with a warning.

- Surface the probe in the startup capability string (EVTSTRM-on/off) so
  every log records which wait shape was selected; previously the three
  possible states were indistinguishable in any output.

- Log the first-observed semaphore value in the recovery-timeout message
  as well; the previous message could not distinguish a value that
  changed during the wait from one that never moved.

- Comment corrections: the post-budget wake-on-write claim now states the
  pacing-period bound honestly; the x86 note names the yield fallback on
  CPUs without waitpkg/mwaitx; the event-stream period is stated as a
  kernel-dependent range. Note the previous commit's claim that x86 was
  unaffected was wrong: the snapshot change lets the x86 early-out fire
  where it previously compared a value against itself; the direction is
  an earlier return when the semaphore changed during the prologue.

Device check (Odin 3, ME menu, 30 s): 168.0G instructions, and the new
capability line reads EVTSTRM-on, proving the paced branch was live in
the measured run. Known residuals (ledgered, out of scope): HWCAP is a
boot-time global while the stream enable is per-CPU (migration edge);
no parking-core device has been measured; no automated test covers the
path.
2026-08-14 05:00:59 -05:00
Zulux91 5ef731c9e5 RSX: harden the semaphore event-stream fallback after adversarial review
Findings addressed (blind review, 8 lenses, see PR discussion):

- Gate the fallback on HWCAP_EVTSTRM (new utils::has_wfe_event_stream()).
  The park's wake bound is the kernel's architected timer event stream; on
  a kernel that does not enable it, a monitor-less WFE parks until the next
  unrelated interrupt. Such devices now keep the pre-existing armed-spin
  behavior instead.

- Fall through from the event-stream park to the armed one-shot instead of
  else-ing around it. On cores where the armed WFE parks, this re-arms the
  exclusive monitor every iteration, so wake-on-write is preserved even
  after the spin budget is spent; on Oryon the extra call returns
  immediately and costs nothing measurable. This also shrinks the window
  in which a written-then-overwritten semaphore value could go unobserved.

- Fix the spin's early-out: the call passed a freshly re-read value as
  old_value, which the compiler sank to immediately before the ldaxr,
  making the compare a self-comparison that never fired (verified by
  disassembly). The loop now snapshots its top-of-iteration read and
  passes that, so an already-changed value returns without waiting on
  every core class.

- Move spin_budget under ARCH_ARM64 (silences -Wunused-variable on x86).

- Log awaited and observed values in the driver-recovery timeout message,
  so a timeout caused by a transient value is distinguishable in reports.

- Rewrite the stale comments in place: spin_on_cacheline_once's event-
  stream rationale is core-class dependent (measured non-parking on
  Oryon); wait_for_event's usage rule now covers the sustained-idle
  fallback shape and names the HWCAP_EVTSTRM precondition.

Device check after hardening (Odin 3, ME menu, 30 s): 164.1G instructions
vs 179.5G for the previous commit and 402.7G pre-fix - the win holds.
2026-08-14 04:27:06 -05:00
Zulux91 002a9b274a RSX: fall back to event-stream wait when the semaphore spin does not park
The one-shot cacheline wait (ldaxr-armed WFE) used in semaphore_acquire
does not park on every core. Measured on Snapdragon 8 Elite class (Oryon,
Odin 3): WFE returns immediately while the exclusive monitor is armed
(~28.8M wakes/s in a standalone microbenchmark, vs ~20-30k/s for bare WFE
and sevl+wfe), so the acquire loop ran at ~57M iterations/s through waits
averaging 33 ms - about 99% of the RSX thread's wall time at a menu, with
each iteration also paying the driver-recovery get_system_time() check.

Keep the armed one-shot for the first 500 iterations of a wait - on cores
where it parks it keeps its instant wake-on-write, and where it does not
it acts as a short spin that still catches quick signals - then fall back
to wait_for_event(), which parks on both classes and bounds wake latency
at the architected event-stream period (~50 us measured).

Measured on device (Mirror's Edge, MT RSX on, state-verified windows):
menu instructions -55% (402.7G -> 179.5G per 30 s), played-gameplay
instructions -29% (391.6G -> 279.7G), loop iterations down ~1,400x, wait
counts/durations unchanged, 30 fps frame pacing unchanged (max frametime
34.2 ms). Note: cpu-cycles PMU counts at full clock during WFE park on
this SoC, so cycle-based profiles cannot see this change; measure with
instructions retired.
2026-08-14 03:47:48 -05:00
kd-11 12b1efc266 rsx/fp: Fix decoding of LOOP and REP instructions
- Verified with hardware tests. RSX does not support proper loops.
- The LOOP/REP instruction simply codes a "REPEAT n" instruction for a block of code.
- There is no accumulator register. The compiler emits a preamble inside the loop block to simulate the running counter.
- Oddly enough, the original start and step values are stored in the instruction but are unused. Maybe useful for debugging real hardware?
2026-08-14 11:15:20 +03:00
jpolo1224 39ca5cdab6 0.7.2: settings fixes, Oboe by default, and a working per-section Reset
Per-section Reset did nothing on most tabs. The field lists describe the tabs
as they were before the PS3 rewrite, so Reset was clearing settings the tabs no
longer show while missing most of what they do: Performance listed 22 of 47,
Graphics 45 of 57, Audio 10 of 16 -- audioRenderer, audioFormat, audioChannels
and audioCubebBackend were absent, so changing the audio backend and pressing
Reset was a no-op. Regenerated from what each tab actually writes, mapping
ps3.foo to its ps3Foo key and validating every entry against the serialiser.
Five keys also moved off Graphics because another tab owns them, which was a
cross-tab clobber waiting to happen.

Full Diagonal Range, per stick, on by default. A full diagonal was capped to
the unit circle at ~0.707 per axis, which is what a circular-gated DualShock
really sends -- but games that deadzone each axis separately then ignore
diagonals, and Oblivion's camera crawled diagonally while the cardinals were
fine. Off restores the hardware curve.

Oboe is the default audio backend on Android, with a migration for anyone still
on the old Cubeb default; a deliberate choice of another backend is kept.

Enter Button Assignment (circle/cross) is exposed. The core has always had it
and Android never showed it.

Reset all settings, in General. Per-game overrides and controller binds are
deliberately left alone -- they are invisible from that page.
2026-08-13 22:29:33 -04:00
jpolo1224 b82432c793 VK: allow native fp16 on Adreno drivers that accept it
Oblivion's water did not draw on Vulkan and did draw on OpenGL. The only
Vulkan-only shader workaround in play is the blanket disable of native float16
on every mobile GPU, which emulates it with fp32; its own comment claimed that
"renders correctly", and it does not.

The disable exists for a real failure -- Qualcomm's compiler rejected SPIR-V
containing float16_t and every pipeline came back VK_ERROR_UNKNOWN, which
presents as a black screen with working audio and a working compile overlay,
so it reads as a renderer bug rather than a shader one. That is not worth
reintroducing blind, so this is a version gate rather than a removal:

  Adreno on driver 512.676.53 or newer -> native fp16 (verified)
  older Adreno                         -> unchanged
  Mali, PowerVR, Xclipse, the rest     -> unchanged, untested either way

Found by switching the renderer to OpenGL, which isolated it to the Vulkan path
in one run after the settings-level suspects had all come back empty.
2026-08-13 22:29:17 -04:00
jpolo1224 7f54855b7d lv2/vm: fix a read-only unlink lockup, and three log floods
sys_fs_unlink handled notdir and noent but not readonly, so it fell through to
fmt::throw_exception and killed the PPU main thread inside the syscall. The
emulator then sat with nothing to run: the game froze with the CPU at 1% and
nothing in the log but a stalled RSX. On Android /app_home is the mounted ISO,
which is read-only, so any game deleting a file in its own directory hit it --
Oblivion removes warnings.txt at startup and never got past it. Returns
CELL_EROFS now, which is already what sys_fs_write and friends do. sys_fs_mkdir
and sys_fs_rmdir carried the identical block and are fixed with it.

Three log floods, all of which stall the emulator outright because writing them
is not free on Android:

- sys_fs_utime logged two warning lines per call and rides a polling loop.
  Oblivion's FileCaching thread hit it 7274 times in ten seconds on one .BSA,
  ~22k lines, and the frame loop stopped for over twenty seconds. Now trace.
- vm::lock_sudo reported a failed mlock on every mapping. Android never grants
  RLIMIT_MEMLOCK to apps, so it fails forever while advising the user to raise
  a limit they cannot raise -- 6470 lines in ten seconds here, and ~1200 in
  every other game log looked at. Reported once per session now.
- sys_mmapper's map/unmap pair, 12431 lines over the same window. Now trace.

None of them lose information: raise the channel to Trace to get them back.
2026-08-13 22:29:06 -04:00
jpolo1224 0819f1ef15 RSX: return the renderer to the 0.6 path, keeping the FIFO idle fix and ADPF
Testers consistently report the best performance on the build with the 0.6
renderer, so 0.7's graphics work goes back out. The Arkham City measurement
behind it (62.8 -> 51.2 ms) was one game on one device and did not survive
contact with a wider set of hardware.

Two files are kept from 0.7 because neither is render pass work and both are
measured wins on their own: RSXFIFO's idle spin plus WFE park, which took ~11%
of total CPU off sched_yield, and RSXThread's ADPF feed, without which the
performance-hint setting reports nothing and does nothing.

Everything else under Emu/RSX is byte-identical to 0.6. The removed work is not
lost -- it is in c4b45eee2 and can come back a piece at a time with testing
behind each one, which is how it should have gone in the first place.
2026-08-13 22:28:52 -04:00
Megamouse 4c63acfb40 Qt: Add unofficial build warning 2026-08-14 02:07:18 +02:00
jpolo1224 8ee20d91d5 0.7.1: remove vertex cache retention, keep the rest of the renderer
The previous commit reverted all of Emu/RSX to 0.6, which was more than the
bug required. Bisecting had already shown the render pass work was not
responsible -- reverting it alone changed nothing, while removing retention
with the render pass work in place fixed both reported games.

So only retention goes. It reused vertex cache entries across frames, and on
0.6 the attribute ring was too small for it to engage; raising the ring to
192M switched an existing path on in every game at once and handed draws
stale geometry. Back to purging every frame, as 0.6 did.

This restores what the wider revert had taken out for no reason: the render
pass reduction, the RSX FIFO idle fix, ADPF frame timing, the ZCULL and
occlusion query fixes, the swapchain and surface lifetime ports, and VRAM
budgeting.

Sonic Unleashed still does not render FMV cutscenes. That reproduces with
the 0.6 renderer too, so it is unrelated and still open.
2026-08-13 19:51:48 -04:00
jpolo1224 4797ad8a9a 0.7.1: revert the 0.7 renderer to 0.6
0.7 introduced corruption in several games that were fine on 0.6 -- flashing
and flickering in Sonic Unleashed and Dragon Ball among others. Emu/RSX is
returned to its 0.6 state in full; everything outside the renderer is kept.

The main cause was vertex cache retention. On 0.6 the attribute ring was too
small for retention to engage, so raising the ring to 192M did not add a code
path, it switched an existing one on in every game at once, and reusing stale
vertex data is what the flashing was.

Bisecting also showed the render pass work was not responsible: reverting it
alone changed nothing. It can come back, but on its own and with testing
behind it rather than as part of a batch.

Kept from 0.7: the ARM64 PPU float to integer fix, the PPU cache build
identity, the SPU checksum and block state fixes, Oboe, ADPF, and the crash
and stability ports.

Sonic Unleashed does not render FMV cutscenes. That reproduces with the 0.6
renderer as well, so it is not from any of this and is still open.
2026-08-13 19:46:13 -04:00
Megamouse 3f4364fe74 Qt: Decrease layout margin in settings_dialog 2026-08-14 01:02:56 +02:00
jpolo1224 c4b45eee27 0.7: SPU and RSX fixes, Oboe audio, and the ports from ouroboros420 and rfandango
SPU: the ARM64 block checksum folded two thirds of every block through
absolute difference, which is not injective, so adding the same value to
two words left the checksum unchanged and similar job binaries hashed
alike. Plain summation now. This is what Precise SPU Verification was
working around, and that setting is exposed properly instead of only being
reachable by hand editing the config.

SPU: a block is no longer marked permanently failed when the trampoline
rebuild fails. The compiled function was live, the state was not
recoverable for the rest of the session, and the claim could never be
retaken.

RSX: render pass churn cut in heavy scenes, roughly 113 to 85 passes per
frame. On a tile based GPU every pass boundary is a full tile store and
reload. Two Vulkan specification violations fixed, and a read/write hazard
on the render pass path.

RSX: the FIFO no longer burns a core on sched_yield while idle.

Android: ADPF is implemented rather than an inert setting, logcat no longer
allocates and makes an IPC call per line, and Silence All Logs is available
for playable titles.

Audio: Oboe backend, for the per device quirks database and stream recovery
on disconnect and route change.

Ported from ouroboros420/rpcsx: GPU Turbo, power and thermal handling, the
crash and freeze fixes, savestate and WSI surface lifetime, honest RAM VRAM
budgeting, the persistent SPU object cache design, occlusion query and RSX
fixes, frame pacing and tiler tuning.

Ported from rfandango/rpcsx: the Turnip ZCULL deadlock fix and ARM64 SPU
checksum handling.

Individual commits are credited in comments at each site.
2026-08-13 18:36:41 -04:00
jpolo1224 87ccdb8515 PPU: stop inverting float-to-int saturation on ARM64
FCTIW, FCTIWZ, FCTID and FCTIDZ carried a saturation correction that only
makes sense on x86. cvtsd2si returns 0x80000000 for any value it cannot
represent, so the result is XORed back into 0x7fffffff on overflow.

FCVTNS and FCVTZS already saturate on their own, so the same XOR turned a
correct result into its opposite: every overflowing conversion produced
INT_MIN where it should have produced INT_MAX. The mask is a no-op when
there is no overflow, so it never did anything except break that case.

Armored Core: For Answer put the player under the floor in the tutorial
because a coordinate that should have clamped high arrived clamped low.

The cache needed a build identity as well. Its key is the executable's
SHA-1 plus a settings bitset and nothing more, so the first attempt at this
fix silently reused objects compiled by the previous build and looked like
it had done nothing. Every earlier PPU codegen change had the same problem
for anyone with a warm cache.

Module loading also no longer abandons the remaining modules after one
object fails to load.
2026-08-13 18:36:26 -04:00
Ani ee43ab7362 Revert "[SPU LLVM] Decrease SHUFB's constant generation target requirements"
This reverts commit 26e37d8c8c.
2026-08-14 00:19:17 +02:00
Megamouse 2dc6cf014c Qt: add Open Custom Gamepad Config Folder action to game list context menu 2026-08-13 21:36:51 +02:00
Megamouse c285c2fb41 Qt: fix settings_dialog tab index 2026-08-13 19:14:26 +02:00
Megamouse c41595e79f Qt: implement auto_scroll_label and use it for the settings descriptions 2026-08-13 14:42:31 +02:00
Lalit Shankar Chowdhury bf541b5828 qt: make settings description static 2026-08-13 14:42:31 +02:00
Megamouse 41e2d101e7 Update discord-rpc 2026-08-13 11:55:39 +02:00
kd-11 c27b38f300 vk: Run GC on the driver manager thread 2026-08-13 11:39:34 +03:00
kd-11 5bd7fd7817 vk: Implement an asynchronous driver manager thread 2026-08-13 11:39:34 +03:00
kd-11 7f9b8d23cd vk: Enhanced thread safety when handling the query subpool allocation cache 2026-08-13 11:39:34 +03:00
kd-11 01e3fed466 vk: Enhanced thread safety when handling descriptor subpools 2026-08-13 11:39:34 +03:00
kd-11 ea0e10e704 vk: Ensure all drawable surfaces invalidate fbo cache on deletion 2026-08-13 11:39:34 +03:00
kd-11 c2eb11eae6 vk: Seal some data leaks with the framebuffer cache
- Maybe fixes some device lost crashes when running in VRAM-constrained situations
2026-08-13 11:39:34 +03:00
jpolo1224 e10f846924 App: make pause, restart and the FPS cap do what they say, and add trophies
Pause reached the core for the first time. Rpcs3Bridge.pause() set a bool and
returned, on the belief that RPCS3 has no explicit pause entry point -- Emu.Pause()
exists and _rpcsx_surfaceEvent has always called it on surface loss, which is why
backgrounding the app was the only thing that paused. Exported as _rpcsx_pause
through all four layers; resume already reached the core, so the pair was asymmetric.

Restart no longer crashes: setCustomDriver dlclose'd the previous driver handle, and
applyRendererPrefs re-applies the driver on every start, so restart unloaded the
library VMA had resolved vkGetPhysicalDeviceMemoryProperties2 out of. ~VKGSRender then
freed its heaps and UpdateVulkanBudget called into an unmapped mapping. An ICD cannot
be unloaded while anything resolved from it is reachable, so it is no longer closed.

Restart no longer returns to the library either: shutdown() set stopRequested, called
kill() and returned with the VM still live, so the run loop's finally started the
replacement and the in-flight teardown killed it -- two BootGame calls, then Unloading
ISO, by which point the restart flag was spent. shutdown() now waits (bounded) for the
core to report Stopped, and the restart is queued on vmStopControl behind it.

FPS cap applies at every value. ConfigStore recorded a persistent core override of
Video@@Frame limit=60 and Settings rewrote it on every push, both from a migration
escaping a stored 120 -- but that node is the cap control, and overrides replay last,
so presets were pinned at 60 while 20 and 45 worked through Second Frame Limit. The
Vblank Rate force stays, since Frame limit Auto resolves to it. Stale overrides are
cleared once. 90 and 120 dropped from the row: the min() in the pacer discards them.

Cover art for PKG installs: the library grid's fallback chain stopped one leg short of
the extracted ICON0.PNG while the in-game menu's did not. Both now share one chain, so
they cannot diverge again. has()/discIconFile require bytes rather than existence, and
the staging rename is checked instead of discarded.

Licences are grouped per game and collapsed instead of a flat list of content ids.

Trophies: a library-wide browser and an in-game tab for the running title, reading
TROPCONF.SFM and TROPUSR.DAT directly -- no account, no network. The in-game set is
identified from the core's own current_trophy_name (try_get, since get<> would
construct it outside emulation and hand back an empty name), falling back to TROPDIR
on disk because a game registers its context lazily. Note the entry stride there is
16 + entries_size, not entries_size.

Also: renderer.upscale.label was defined twice, so Internal Resolution was dead.
2026-08-12 16:46:01 -04:00
jpolo1224 db6ee86806 Core: survive a module LLVM cannot compile, and fill in the Android string table
PPU: a module that fails codegen no longer takes the boot with it. ppu_initialize2
called the fatal jit.add(); run_recoverable_llvm and the try_* pair already existed
in this tree but were used only by the SPU recompiler, so LLVM's fatal handler threw
on a thread with no recovery context and killed the worker. That is not one lost
module: g_progr_pdone is incremented in the compile loop's INCREMENT, so the module
the dead worker held was never accounted for, g_progr_ptotal could never reach zero,
and the boot waited on it forever. Saint Seiya: The Sanctuary (BLES01421, issue #25)
stopped at 133 of 134 on 'Cannot scavenge register without an emergency spill slot'.
Now routed through try_add on ARCH_ARM64, mirroring the SPU branch, with
ppu_initialize2 returning bool so the caller stops logging a dead module as compiled.
Losing the worker also halved the rate for everything left.

VK: a data_heap block no longer frees through an allocator that is not the current
one. Borrowed pointer, cached at construction with nothing tying it to the
allocator's lifetime; declining the free costs nothing the device teardown does not
already release.

Android: g_strings held 180 of localized_string_id's 323 entries and the callbacks
ignored their args entirely, so every string carrying a name, date, size or error
code lost it -- including CELL_SAVEDATA_LOAD, which is why the save prompt was Yes
and No over an empty message (open_msg_dialog logged msgString=""). All 322 the Qt
switch provides are present, in enum order, with QString::arg's %0 substitution
reproduced and utf8_to_u32string on the u32 path so trophy names survive. A
static_assert on the table size fails the build when upstream adds an id.

probeDiscInfo: set g_fxo up before mounting. vfs::mount lazily constructs vfs_manager
through manual_typemap::init<T>(), which writes *m_order++, and clear() nulls that
when a game stops -- so scanning a new disc image after playing anything wrote
through null. Emu.IsStopped() cannot guard it, because stopped is the cleared state.
2026-08-12 16:45:29 -04:00
jpolo1224 708582e523 Ship the ANGLE libraries from the module that actually builds
libEGL_angle.so and libGLESv2_angle.so lived in armsx3-app, which stopped being the
built module, so selecting ANGLE for the OpenGL renderer silently fell back to the
system driver with nothing in any log to contradict it. Moved into armsx3-ui beside
the core, with the jniLibs .gitignore negations that keep them tracked.

verifyAngleLibs comes with them and now runs on the release graph ahead of
mergeReleaseJniLibFolders, so packaging an APK that offers ANGLE without shipping it
fails the build. The copy left behind in armsx3-app could never have protected
anything from there, and did not even compile -- its GradleException message escaped
'$' as if the file were a template, and the quotes inside the escaped interpolation
closed the string early, so the project failed to configure. Deleted rather than
fixed, with a comment pointing at the live one.

Version to 0.6 (versionCode 10).
2026-08-12 16:45:12 -04:00
jpolo1224 8a6deab362 Touch: put the tap-to-reveal pause row back in the in-game menu
Its comment was still there, above the OSD selector, describing a control that no
longer existed -- the row was lost in the port and the setting left with no writer,
so nobody could hide the glyph or bring it back. Reported as the option missing from
the menu, which is what it was.

Goes where the comment says rather than in the touch editor toolbar, which is where
I first put it: this is a pause-button behaviour toggle and belongs with the overlay
controls it was written for.
2026-08-12 10:22:35 -04:00
jpolo1224 5b740f8921 Touch: give tap-to-reveal pause a control
The setting has existed since the pause button moved to the top right, and is seeded
once from the old show/hide pref so anyone who had the button hidden keeps it hidden.
Nothing ever wrote it afterwards. A user whose button was visible had no way to hide
it and a user migrated into hidden had no way back, which is how it was reported:
the option is not in the in-game menu.

Sits with multi-touch, gliding and floating stick in the editor toolbar, since those
are the other whole-overlay behaviour toggles and setPauseTapToReveal already
existed to be called.
2026-08-12 10:16:05 -04:00
jpolo1224 11f043b529 VK: retire completed frames on flush again, so the upload rings reclaim
The freeze-with-audio in Ratchet & Clank is the RSX thread dying in the allocator,
and the heap growth log says why. The index buffer went 16M to 64M to 128M to 192M
to 256M inside 290ms, on requests of 2K, 4K, 5K and 3K; the attrib buffer did the
same and died growing to 192M. Kilobyte allocations cannot need a quarter gigabyte.
The rings were never wrapping, they were only ever growing.

frame_context_cleanup is what returns a frame's ring memory, and check_present_status
is what calls it. I removed that call from flush_command_queue in 0.5 because the
drain poked the oldest queued frame's fence and on Adreno vkGetFenceStatus blocks
until signalled instead of answering -- 14.6ms a frame, second only to the FIFO decode
loop. The reasoning was that the flip path retires frames anyway. It does, enough to
keep presenting, but not often enough to keep the rings bounded, and nothing else
reclaims them.

Restoring it costs nothing now. poke() no longer asks with vkGetFenceStatus: it uses
vkWaitForFences with a zero timeout, which is specified to return VK_TIMEOUT without
waiting. The measurement that motivated the removal was of the old implementation, so
the speedup stays and the reclaim comes back.

Keeps the heap growth log that found this. The allocator reports only a size and a
pool number, and pool 1 covers every data_heap, so three fixes were aimed at a target
that could not be seen. One line naming the heap settled it.
2026-08-12 10:02:13 -04:00
jpolo1224 b8987b8c92 Revert "VK: grow upload heaps in steps a phone can actually place"
This reverts commit 403df1c651.
2026-08-12 09:54:00 -04:00
jpolo1224 403df1c651 VK: grow upload heaps in steps a phone can actually place
Ratchet & Clank freezes with audio still playing, which is the RSX thread dying:
'Failed to allocate 131072K of video memory (pool=1, pool total=561M, heap cap=
2048M)'. Pool 1 is VMM_ALLOCATION_POOL_SYSTEM, and 131072K is a data_heap taking
its second growth step, 64M to 128M.

The device is not out of memory. It holds 561M against a 2048M cap and cannot place
128M in one piece, which is a different failure from being full and has a different
fix. The heap grew by aligning up to 64M, so every growth demands a single
contiguous block of at least that size, and each step doubles what the allocator has
to find unbroken. A heap that fails to grow has nowhere to degrade to, so the
renderer ends there.

Android now grows in 16M steps and stops at 256M. The smaller granularity asks for a
quarter as much contiguous memory per step and lets the heap settle near the size
actually wanted rather than overshooting to the next 64M boundary. The ceiling comes
down to match: a 1GiB upload ring would exhaust the device long before it was
reached, so as written it was a limit only reachable by dying. Desktop keeps 64M and
1GiB.

Does not touch the separate pool-0 case fixed in the previous commit, where recovery
does run and the last-ditch eviction now gets a turn before the thread is killed.
2026-08-12 09:50:26 -04:00
jpolo1224 614cdd74a3 VK: evict everything before ending the RSX thread, not after
Ratchet & Clank freezes with audio still playing, which is the RSX thread dying on
VK_ERROR_OUT_OF_DEVICE_MEMORY while the rest of the process lives. Caught on an
Adreno 740: 'Failed to allocate 86016K of video memory (pool=0, pool total=472M,
heap cap=2048M)'. One 84MB request refused while we held 472MB of a 2048MB cap, so
the heap was not full -- a single large allocation could not be placed.

The allocator already retries once after asking for pressure relief, and it did.
The relief is what fell short. on_vram_exhausted refuses the hard sync whenever the
RSX is uninterruptible, and clamps the request below fatal, so the eviction that
drops everything unlocked was unreachable from here. That refusal is right while
there is still a way out: eviction touches resources the driver may still be
reading. It is wrong on the last attempt, where the alternative is not a glitch but
the renderer ending.

So the final attempt is now exempt, through a thread-local set only for the width of
that call. Everything else keeps the existing behaviour, and a caller that opted out
of recovery is not handed it here by the back door. Recovery logs at error level and
says a visual glitch is the expected outcome, since a silent recovery that costs
texture quality reads as a new bug otherwise.

Measured against this failure the eviction ran once, six microseconds before the
allocation failed, and never in the five minutes before it -- so nothing was
reclaimed while it still would have been cheap. That part is not addressed here: the
budget-based ladder cannot see mobile unified memory, where the driver reports one
large shared heap and 472MB against it never crosses a threshold. This makes the
failure survivable rather than preventing it.
2026-08-12 09:42:11 -04:00
jpolo1224 76af990eed VK: stop reserving a desktop-sized descriptor cache on Android
A heap profile of Ratchet & Clank on an Adreno 740 put the only real growth during
play on vk::descriptor_set: 56 sets created in half a session, 49MB, through
simple_array::reserve from descriptor_set::operator=. Nothing else grew that was
not one-time JIT or shader compilation.

Each set reserves m_pool_size entries in three pools the first time it is used --
16448 image infos, 16448 buffer infos, 16448 buffer views, about 920KB -- and there
is one set per shader program. simple_array::clear() only resets the size, so that
memory is held for the object's whole life, and the total climbs for as long as new
pipelines keep appearing. Ratchet compiles a lot of them.

It is also why this was invisible from the Vulkan side: these are plain malloc, not
device memory, so the VMM never sees them and no amount of texture eviction reclaims
them. The tester's log shows the shape exactly -- our pool at 516MB while the process
walked from 4448MB to 5626MB without ever dropping, then died on a 32MB allocation.

The reservation is a correctness requirement, not a tuning knob: push_*() hands
Vulkan the address of a pool entry and it has to stay valid until flush(), so the
pools must not reallocate while writes are pending. What makes it safe to shrink is
that max_cache_size is also the flush threshold, in both on_bind() and
storage_cache_pressure(), so the queue can never outrun the reservation. Moving it
takes the guard with it and leaves the same 64 entries of headroom.

1024 on Android: about 57KB a set instead of 920KB, for one extra
vkUpdateDescriptorSets per 1024 writes. Desktop keeps 16384.
2026-08-12 09:05:39 -04:00
jpolo1224 441c3f1bde Merge PR #34: vectorize the primitive-restart index upload on ARM64 2026-08-12 09:03:52 -04:00
Zulux91 fcdd7cd6af RSX: static_assert the all-ones identity the NEON restart lanes rely on
The vector body stores vorrq(v, eq) into restart lanes, which is all-ones
regardless of what index_limit() returns -- it only matches the scalar
tail's index_limit store because index_limit is all bits set. Assert that
beside the splats so a change to index_limit fails the ARM64 build instead
of silently diverging the vector body from its own tail. No codegen change
(emitted assembly is identical).
2026-08-12 07:49:32 -05:00
Walter 26e37d8c8c [SPU LLVM] Decrease SHUFB's constant generation target requirements
The constant generation AVX512-ICL path only requires the feature GFNI, which exists on non-AVX512 CPUs. This was a hold-over from when the shuffle step was merged together. (The proceeding unsigned minimum is from SSE2)
2026-08-12 15:38:43 +03:00
Zulux91 ad15f81d6e RSX: clarify comments on the ARM64 index-upload paths
Explain why the non-restart loop stays scalar (clang already
auto-vectorizes it) and spell out which allocations each caller passes,
so the no-overlap contract of upload_untouched_neon is checkable.
2026-08-12 07:24:20 -05:00
Lalit Shankar Chowdhury 9b1eb45a47 PPU: implement AVX2 path for gv_rol32 2026-08-12 14:01:33 +03:00
Zulux91 2df75aa604 RSX: vectorize the primitive-restart index upload on ARM64
The primitive-restart variant of upload_untouched had no SIMD path on
ARM64: the asmjit builder is x86-only, and clang cannot auto-vectorize
the scalar loop (-Rpass-analysis: "value that could not be identified
as reduction is used outside the loop") because the min/max updates are
conditional on the restart compare -- while the non-restart loop next to
it does auto-vectorize. Net effect: 16 scalar instructions per index on
a path some titles saturate. Measured on a Snapdragon 8 Elite (Odin 3),
Virtua Tennis 4 routes its entire indexed-draw traffic through this
loop: 2.81 billion indices in a 9-minute match session, median 159k
indices per frame.

Port the x86 lane algebra to NEON, 8x u16 / 4x u32 per iteration: the
restart-equal mask ORs the lane to all-ones for the min accumulator and
the store (all-ones is index_limit, exactly what the scalar loop
writes) and BICs it to zero for the max accumulator, so restart lanes
can never win either reduction; UMINV/UMAXV reduce once at the end and
the tail stays scalar. Baseline v8.0 AdvSIMD only.

Supporting results, all on the Odin 3 with the system driver:

- Correctness: 216-case differential (scalar vs NEON vs the dispatched
  path; every tail residue mod 8 and mod 4; restart index absent,
  present, 0, index_limit, all-restart, and index_limit present while
  not the restart value; u16 and u32) ran on device at RSX init in all
  four A/B runs: 0 mismatches. An independent 65,000-case host-side
  model of the same lane algebra also matched the scalar loop, and a
  blind review of the diff could not construct a diverging input.

- Performance A/B (cntvct_el0 around the dispatch, null-region
  calibration subtracted, per-window medians over 120-flip windows with
  >10k restart indices/flip, runs interleaved scalar/NEON/NEON/scalar):
    scalar: 0.02504 and 0.02464 ticks/index  (1.30 ns/index)
    NEON:   0.00346 and 0.00381 ticks/index  (0.19 ns/index)
  ~6.8x faster per index; scalar-scalar repeatability 1.6%. Worst
  single-frame cost in this loop fell from 3.64 ms to 0.99 ms. FPS
  stayed 60/60 in all runs on this device; the win is RSX-thread
  occupancy and worst-frame cost, and would be frame time where the
  RSX thread is the bottleneck.

Titles that never enable primitive restart are unaffected: they route
through the untouched path, which clang already vectorizes.
2026-08-12 05:53:44 -05:00
jpolo1224 b0c7a0260e VK: judge memory pressure against what the driver will give, not our own cap
vmm_determine_memory_load_severity is a set of thresholds on get_memory_usage,
which is usage/budget straight out of vmaGetHeapBudgets. VK_EXT_memory_budget was
never enabled, so VMA had no budget from the driver and used the heap size -- and
where pHeapSizeLimit is set, that limit, which is our own vram_allocation_limit.
An Adreno 740 log shows the consequence: 516MB against a 2048MB cap is 25%, below
even the 50% mark, so the allocator kept its fastest flags, severity stayed 'low',
and the 75/90/95 eviction ladder never fired. The first allocation the driver
refused was also the first sign of trouble, and that one is fatal.

Enabling the extension gives VMA the driver's own estimate. VMA takes the smaller
of it and pHeapSizeLimit, so the cap still caps -- it just stops being mistaken for
headroom that exists. Gated on support and logged when absent.

NOT a fix for the Ratchet & Clank crash this was found in, and it should not be
credited as one. That log leaks about 60MB per sample, 4448MB to 5626MB with no
drop, while our own pool sits at 516MB -- so nearly all of it is outside anything
VMA can see or evict, and a truthful budget only makes us give up our own memory
sooner. The tester reports 0.4 unaffected, which makes it a 0.5 regression still
to be found; see the Adreno per-vkCmdEndRenderPass allocation already recorded
against this codebase.

Also: licences can be removed. Installing one was one-way -- the row existed only
to prove the install had happened -- so a wrong or duplicate .rap could only be
cleared through a file manager, which on a scoped-storage device most people
cannot do at all. Confirmed before deleting, like uninstalling a title, because
content stops working without it.
2026-08-11 23:43:23 -04:00
jpolo1224 7745a3c92d Apply the README trim from PR #28
Reverted during the merge on the assumption it was a contributor's deletion; it
was a deliberate one, so take it as authored.
2026-08-11 22:01:43 -04:00
jpolo1224 b09815e595 Pad: carry analog button pressure through to the game
Every pressure-capable button was fully digital. _rpcsx_overlayPadData ended with
btn.m_value = m_pressed ? 255 : 0, and that value is what cellPad copies into the
press byte a game reads for an analog button, so no half-press could ever reach
one. Rpcs3Bridge.setPadButton threw the magnitude away before that, using `range`
only for stick directions and calling applyButton -- pressed or not -- for
everything else.

Two features were silently dead as a result. A physical L2/R2 went 0 to 100 like
a digital button, reported on Iron Man, whose level-two hover tutorial cannot be
passed without a half-press; the trigger axis was read and scaled correctly all
the way to the JNI boundary and discarded there, which is why remapping and
recalibrating changed nothing. The touch overlay's pressure modifier had the same
end: it computes a range through pressureRangeFor and hands it to the same call.

Pressure now travels as its own export rather than widening overlayPadData, whose
signature is frozen -- the core is dlopen()ed and updated independently of the JNI
glue, so a wider existing export would have older glue passing a garbage argument.
Glue or core predating _rpcsx_overlayPadPressure keeps the old digital behaviour.

0 means "nothing analog drives this button", which is a safe sentinel rather than
a lost level: an unpressed button already reports 0, so a pressed button at 0
cannot occur, and the zero-initialised array is exactly the previous behaviour.
Pushed only when it changes, so an all-digital pad adds no JNI call per event.

All twelve buttons the PS3 pad reports pressure for, not just the triggers, since
the offsets are contiguous and cellPad already routes each one. sendTrigger also
floors to at least 1: the lightest real squeeze truncated to 0, which is the input
layer's "full press" convention and would have delivered the opposite of a
half-press.
2026-08-11 21:53:09 -04:00
jpolo1224 3791865e2f Merge PR #28: make a Vulkan driver that stops answering diagnosable
Rebased by the author onto 0.5, so the occlusion-query bound we shipped stays as
it is and this only adds diagnostics on top of it: the fatal throw that ended the
session is gone, and with it the Web of Shadows regression that kept both PRs out
of 0.5. Also leaves the wait on shutdown, so a driver that never answers cannot
wedge the exit.

README.md is deliberately not taken from the PR -- it removed the whole status
and differences-from-upstream section.
2026-08-11 21:47:58 -04:00
jpolo1224 6831c87a0e Merge PR #23: fix PS3 patch state reporting and verify downloaded patches 2026-08-11 21:47:43 -04:00
Zulux91 b4378d8977 Say why a custom driver cannot load, before trying to load it
adrenotools swallows this failure. When its dlopen of the custom driver fails it
logs to logcat and hands back the system driver, so the load looks successful
from here, the reason never reaches the emulator log, and the user runs a driver
they did not choose while believing otherwise. The existing dlerror() report
cannot fire, because the pointer that comes back is not null.

That cost real time. Mr Purple T29 fails on an Android 15 device with "cannot
locate symbol pthread_getaffinity_np", falls back, and every log looked exactly
like a successful custom-driver session -- I recorded it as passing a driver
comparison it had never taken. The only hint was its reported driver version
matching the system driver's, which took three saved logs side by side to spot.

The requirement is stated in the file. DT_VERNEED lists the libc versions a
binary needs, and T29 needs LIBC_36, meaning API 36, on a device that provides
35. Reading that before the attempt turns "failed to load" into the reason, and
covers the whole class of community drivers built against a newer NDK than the
device runs -- likely the most common way these packages fail.

Metadata cannot answer this: T29's own meta.json declares minApi 30. That field
is author-declared and unverified, so only the binary is trustworthy.

Reported through the emulator log as well as logcat. The UI glue can only reach
logcat, which is not the file anyone attaches to an issue -- the reason would
exist and no report would ever contain it. It lands beside the driver identity
that it explains.

Advisory on purpose. The load is still attempted and nothing is rejected, so a
wrong answer here costs one log line and never a working driver. It stays quiet
unless it positively finds a LIBC_<n> requirement above the running API, and
declines to answer at all when section headers are absent.

Verified on device both ways: T29 reports needing LIBC_36 against API 35 in
RPCSX.log, and stevenmxz v33, which loads correctly, produces nothing.
2026-08-11 19:59:51 -05:00
Zulux91 8fed09d40e Say which way an abandoned occlusion query was failing
Upstream now bounds this wait itself: warn at one second, abandon at
three and use whatever the query holds. That replaces the fatal timeout
this commit previously carried, and it is the better answer -- the throw
could end a session over a driver that was merely slow, at worst wrong
culling for a frame was the actual cost. What remains here is the part
the bound does not cover:

- On abandonment, ask the driver once more directly with
  VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and log which way it is
  stalling: VK_NOT_READY, or VK_SUCCESS with the availability word still
  clear. The two are indistinguishable through poke_query and need
  different conversations with whoever maintains the driver.

- Leave the loop when emulation is aborting. A driver that never answers
  must not also wedge the exit path, and the value is irrelevant once
  the session is going away.

Found chasing a Skate 3 freeze on an Adreno 830, where stevenmxz's gen8
driver builds accept occlusion queries and never complete them; the
system driver completes them in microseconds.
2026-08-11 19:59:51 -05:00
Zulux91 8772786f9a Say which Vulkan driver actually answered
The startup log named the GPU and a driver version, and on Android neither
identifies the driver. adrenotools' hook falls back to the system driver when
its dlopen of the custom one fails, and reports that only to logcat, so a
session that silently ran the system driver logged exactly the same thing as
one that ran the custom driver it was asked for.

That is not hypothetical. Chasing a Skate 3 freeze on an Adreno 830 I recorded
a custom driver as passing a test it never took: it had failed to load with
"cannot locate symbol pthread_getaffinity_np", fallen back, and the log still
said the custom driver was bound. The only tell was that its reported version
matched the system driver's exactly, which needed three saved logs side by side
to notice.

Logs the driver identity Vulkan already reports -- name, driverID, info and
conformance version, all of which were being fetched and thrown away -- and
falls back to saying the identity is name-derived when VK_KHR_driver_properties
is missing, which is common on the older Android devices this matters most on.

Where a custom driver was requested and Qualcomm's own driver answered, that is
a silent fallback, since adrenotools installs Mesa/Turnip builds. It now says
so, and points at the logcat line carrying the actual reason.

The loader's own message no longer claims more than it knows: the handle it
binds is the one it was handed, and whether the driver behind it is the
intended one is not something it can see.
2026-08-11 19:57:37 -05:00
kd-11 92870a3d4e rsx: nv0039 cleanup
- Enforce some behavior observed on real hardware
2026-08-12 03:33:10 +03:00
jpolo1224 5405d71e2e Update README.md 2026-08-11 18:12:01 -04:00
jpolo1224 93c6df7e42 Settings: clear the core tuning left pinned while debugging
Raw core overrides re-push after the curated settings, so a stale one silently
beats the UI with nothing on screen to explain it: the settings screen read SPU
Block Size = Safe for hours while config.yml read Mega.

Mega is the one that mattered. It produces very large compilation units, and those
are what fail AArch64 register allocation with "Cannot scavenge register without
an emergency spill slot" -- which is what put SPU threads on the interpreter
fallback at all. With it cleared, no block fails to compile and the fallback never
engages. Every "cannot be compiled" chased in these sessions traces back to it.

Cleared in every scope, because a title can pin a key the global also pins: Arkham
City carried Accurate SPU Reservations true as a raw per-title override against
false globally, so clearing one scope did nothing and the two readings looked
contradictory.

Per-title Accurate SPU Reservations values go too, except Web of Shadows, which is
the title it was measured on. Off is off-spec -- it forces the SPURS scheduler to
HLE and bypasses the reservation lock -- and a title left that way desyncs until
its SPU threads execute whatever they land on, which is how Arkham City ended up
dying with "Unknown STOP code: 0x0".

Adds CoreSettingOverrides.forgetEverywhere for the all-scopes case.
2026-08-11 15:54:54 -04:00
Nick Gregory f7cfdc6570 rsx: Fix nv0039 image (de)interleaving functionality 2026-08-11 19:40:15 +00:00
jpolo1224 f056d6fc86 Video: keep the VRAM heap cap at 2048
3072 was set to get the God of War 3 demo past an allocation failure, but that
failure was measured before the uninterruptible reclaim fix landed, and the cap is
not coordinated with the texture cache, which budgets itself up to 2560MB on
Android. Raising one without the other let the total grow with it: Batman: Arkham
City reached 5596MB resident against a 6246MB peak on a 7.2GB device and stalled
after a while, with no allocation failure to point at.

2048 is the value that shipped before, and it is where the sum of the two sat when
that game worked. Budgeting the cap and the cache together is the actual fix and
is not attempted here.
2026-08-11 15:10:41 -04:00
jpolo1224 ae1caf915b Release 0.5: purge the profiling overrides, raise the VRAM heap cap
The RSX profiler was still recorded as a raw core override from the debugging
work, so config.yml read "RSX Profiler: true" while nothing in the UI said so --
the same divergence as the relaxed-ZCULL one, since overrides re-push at the tail
of applyTo. It writes a bucket report every 300 frames and keeps per-scope timers
on the RSX thread, which is not something to ship enabled. The first purge had
already marked itself done, so this takes a new key.

VRAM allocation limit is applied as VMA's pHeapSizeLimit, which makes it a hard
ceiling rather than an eviction threshold: once total allocations reach it VMA
returns OUT_OF_DEVICE_MEMORY however much the device has free. Lowering it does
not make the cache release earlier, it makes allocation fail earlier. The God of
War 3 demo was measured failing a routine 24MB request at 1024 while the process
held 1.6GB resident and 280MB in that pool, and failing at 2048 one screen later.
3072 leaves the caches room while keeping the bound that stops an unbounded quota
driving the process to 4.3GB and getting it killed.

The allocation failure now names the request size and the cap alongside it, since
"Out of video memory" alone cannot separate a full device from an artificial
ceiling, and those need opposite fixes.
2026-08-11 14:06:11 -04:00
jpolo1224 9128a75c0e VK: reclaim what can be reclaimed when the renderer is uninterruptible
Refusing outright skipped the allocator's own recovery. That path is "if
OUT_OF_DEVICE_MEMORY and vmm_handle_memory_pressure(...) succeeds, retry the
allocation", so returning false meant the retry never ran and the allocation died
having freed nothing: God of War 3 reached it with zero reclaim attempts and zero
recoveries logged.

Only the fatal branch needs the queue idle, which is what the flush inside it is
for. The rest is reachable while uninterruptible: the texture cache purges its
unreleased pool, and at severe it also drops unlocked sections. RPCS3 already runs
exactly that with no flush whenever pressure is non-fatal, so this is the existing
contract rather than a new risk. Severity is clamped below fatal so the
flush-dependent path stays unreachable.

Measured after: eviction runs and reports releasing resources, and the allocator
retries. God of War 3 still fails, but now for the honest reason -- the device is
out of memory, with 123MB free of 7.2GB and the emulator resident at 4.3GB -- and
not because nothing was ever given the chance to run.
2026-08-11 13:55:43 -04:00
jpolo1224 785c4d5627 VK: decline video memory pressure instead of aborting, and budget it lower
on_vram_exhausted asserted that the renderer was interruptible. Eviction really
cannot run in that state, since it would touch resources the driver may still be
reading, but that is a reason to refuse rather than to kill the thread -- and
refusing is already the supported answer: the OOM path in VKDraw treats false as
using placeholder textures, which it notes can cause graphics glitches but
should not crash otherwise.

God of War 3 hit it by skipping the intro screens, which pushes a burst of surface
and texture allocation through a point where the renderer is uninterruptible. The
RSX thread died there, audio kept playing, and it presented as a hang. With the
refusal in place the same run reports the real problem instead:
VK_ERROR_OUT_OF_DEVICE_MEMORY from the allocator.

Which it genuinely is. VRAM allocation limit was also lowered from 2048 to 1024:
the first value was still above what the device could give us -- 5355MB resident,
99MB free of 7.2GB -- so the budget was never reached before the system ran dry,
which defeats its only purpose. It has to sit below what allocation can actually
satisfy, so eviction starts while there is still room to allocate.
2026-08-11 13:45:54 -04:00
jpolo1224 38424a59bd SPU: mark failed program ranges; Video: budget VRAM on mobile
Three things, all found by measurement after the interpreter fallback started
being used in anger.

Marking only the entry point made the interpreter release the thread after one
instruction, whereupon the recompiler tried the next address, failed the same way
and marked that too. 111 consecutive entries were recorded walking two blocks four
bytes at a time, each step paying a full failed LLVM compile. The failed set now
holds ranges, so a thread stays interpreted for the whole block and leaves when
execution genuinely moves past it: 111 markings became 1.

The range test then ran per interpreted instruction and took a reader lock each
time, which put shared_mutex::imp_lock_shared at 28% of the whole process against
23% for the interpreter itself. The extent is now cached on the thread when the
fallback engages, so the loop compares two integers.

The switch was also logged once per thread, but the flag is cleared on every exit,
so the guard fired on every re-entry: God of War 3 wrote thousands of lines a
second ping-ponging between two addresses. Removed; the block is still recorded
once when it is marked.

Separately, VRAM allocation limit was left at upstream's 65536 MB, which means no
limit and assumes a discrete card. Here the GPU shares system memory with the OS
and our own host allocations, so the texture cache is never asked to evict and
grows until allocation fails -- and failing is fatal: God of War 3 dies in
on_vram_exhausted on ensure(!vk::is_uninterruptible() && ...), because VRAM ran
out where the renderer cannot safely evict. Measured at the crash: 5355MB
resident, 99MB free of 7.2GB. 2048 leaves room for the guest's own memory, the
host caches and the OS.
2026-08-11 13:39:05 -04:00
jpolo1224 8c707648cb SPU: leave the interpreter once the uncompilable block is behind us
The fallback flag was set once and never cleared, so a thread that met a single
block it could not compile interpreted everything it ran from then on. Correct,
but these are SPURS kernels doing real work, and Sonic Unleashed reached its
loading screen that way and then crawled through it.

The failed set holds entry points, so this keeps interpreting while pc sits on the
bad entry -- which is where a branch-to-self idle loop stays -- and releases the
thread as soon as execution moves past it. Only the block that cannot be compiled
is interpreted; the rest of the thread runs recompiled.

Leaving is safe at any instruction boundary, since all SPU state lives in
spu_thread, which is the assumption the JIT dispatch already makes. Re-entering
the bad block sets the flag again.
2026-08-11 12:55:02 -04:00
jpolo1224 9543378661 SPU: route the uncompilable-block fallback to the interpreter that works
A block that fails to compile switches its thread to the interpreter. On ARM64
that fallback called spu_runtime::g_interpreter, which with a recompiler selected
is the LLVM-built interpreter, and calling it there executes nothing: measured a
million consecutive calls on Sonic Unleashed's stuck SPURS kernel without pc
moving once. The thread then spins in that loop forever at a fixed pc with no
flags set, which reads as a busy SPU and hangs the title with no diagnostic at
all. Any block that fails to compile landed there, so this was not one game.

old_interpreter is what the static decoder ultimately runs, through
tr_interpreter, and it is self-contained -- opcode table, thread, local store. Its
static-decoder-only check rejected exactly the case that needs it, so it now also
accepts a thread already marked for fallback.

Getting there also needed the give-up paths fixed: the TBL2/TBX2 retry could
return null with an empty error and fall through every branch unmarked and
unlogged, so nothing recorded that a block had been abandoned.

The stall dump now carries SPU event, MFC and interrupt state, which is what made
this findable: parked kernels showed pending=0 (no lost wakeup), intr_en was 0 on
healthy threads too (not interrupts), mfc_q was 0 everywhere (no stuck transfer),
and interp_fb=1 on the frozen thread pointed at the fallback itself.
2026-08-11 12:44:04 -04:00
jpolo1224 0b5a43c602 vm: report who a stuck writer_lock is waiting on
Both waits in writer_lock are unbounded and silent. The acquire loop spins until
every range lock bit clears, and the range_lock path then spins until every
registered PPU thread reaches cpu_flag::wait. A thread that never gets there hangs
every other thread that takes a reservation, and leaves nothing behind: from
outside it reads as a clean guest deadlock with everything in a legitimate wait.

Both now log once, far past any plausible contention, naming the held range locks
or the PPU thread being waited on.

They paid for themselves immediately on Sonic Unleashed, which deadlocks at the
SEGA logo. Both stayed silent across several boots, which ruled out the VM lock
entirely -- worth having, since main_thread was pinned in cellSpursRemoveWorkload
carrying cpu_flag::memory without cpu_flag::wait, which looks exactly like this
bug and is not. The game hangs in a different state on different boots, so it is a
race elsewhere in SPURS.
2026-08-11 10:50:08 -04:00
jpolo1224 5997681fed Packages: install from storage that cannot be read directly, and name what is installed
Issue #16, both halves.

Installing a .pkg or .rap off a USB-OTG drive already went through the system
picker, and the descriptor it returns is handed to the native installer as a raw
fd, so a 40 GB package costs no copy. That holds only while the provider is
backed by real storage. The third-party USB-OTG and cloud apps people reach for
when the platform will not mount their drive return a PIPE, and every install
entry point seeks -- getFileType sniffs the magic and rewinds, package_reader
jumps around the archive -- so lseek failed with ESPIPE and a perfectly good
package was reported as unsupported or broken. Those descriptors are now
detected with the same lseek the core will make, and only those are copied to
real storage first, onto whichever of the emulator's own storage and the app
cache has more room. The copy is checked against the size the provider reported:
a short copy does not throw, it produces a truncated package that fails much
later as "broken", which reads as a bug report about the package.

Split releases picked through the system picker arrived in the order the user
tapped them, and installSplitPkg takes the order given as the part order, so
picking part 2 first extracted into a broken install rather than failing. Both
pick paths now sort the parts, digit runs numerically, since plain string order
puts part 10 between part 1 and part 2.

Installed titles were listed by title id alone -- NPUB90434, BLES01807 -- next
to an Uninstall button, which is where it hurt most: choosing which of two demos
to reclaim space from meant looking the ids up elsewhere. TITLE now comes out of
the install's own PARAM.SFO, read off disk rather than through the library cache
so a title the scanner has not seen yet is still named. The id stays on a second
line because patches, cheats and compatibility lists are keyed by it. Licence
files carry the same id inside their content id, so a .rap can name the game it
unlocks instead of being one of a row of indistinguishable hex strings.

The SFO field reader is the scanner's CATEGORY reader generalised rather than a
second copy of the 16-byte index-entry layout.
2026-08-11 10:43:11 -04:00
jpolo1224 49012ba969 Settings: seed per-title core settings, starting with Web of Shadows
Accurate SPU Reservations off is worth a large amount in Spider-Man: Web of
Shadows and is not safe globally, so it goes in that title's own override rather
than the default.

Its SPURS reservation traffic serialises behind the global exclusive
vm::writer_lock that every reservation_op takes, which no amount of CPU can help:
all six SPU threads and several PPUs were measured yielding at the same rate with
18.8% of total CPU in sched_yield. Off, SPURS takes the lock-free path and
vm::writer_lock fell from 8.06% to 0.96%.

Kept per-title because it is off-spec -- upstream defaults it on, and Sonic
Unleashed fails EARLIER with it off, reaching neither the loading icon nor the
logo, which is consistent with the bypass being the SPURS area itself.

The seed writes only fields a title does not already carry, so a deliberate
change is never overwritten, and it runs once. Other regions of the same game
need their own entry.
2026-08-11 10:26:35 -04:00
jpolo1224 0909f77a95 RSX: charge the empty-ring yield to idle, and stop draining the present queue on flush
Two things, both about the RSX waiting rather than working.

flush_command_queue ended by draining the present queue in case a queued frame
still held a ref to the command buffer just taken. It cannot: next() hands them
out from a 512 entry ring and the queued list is bounded at flip to
m_max_async_frames - 1, so the buffer being reused is hundreds of frames retired.
The guard was unreachable and the cost was not -- check_present_status pokes the
oldest queued frame's swap command buffer, and on Adreno vkGetFenceStatus blocks
until signalled rather than returning VK_NOT_READY, so a poll written to be cheap
became a full GPU sync. 1.32 times a frame at about 11ms: Fence poll 14.6ms ->
0.033ms, frame 44.5ms -> 36.6ms. Same fault as the two sites removed earlier;
this one sat inside flush_command_queue rather than on the present path. Ruled
out first: identical frame time at quarter resolution, and forcing the swapchain
pre-transform to match the surface left it unchanged.

The empty-ring yield is now charged to idle. It sits inside fifo_decode, which is
the enclosing scope of the whole run loop, so waiting on an empty ring was
reported as decode work -- Idle 0.003ms against FIFO decode 20.4ms, while a
native profile of the same thread put 34% of its cycles in sched_yield. The
bucket report and the profiler disagreed and the bucket report was wrong, which
has now produced two wrong conclusions in one session.
2026-08-11 10:13:53 -04:00
jpolo1224 0cf85cb902 RSX: drop the FIFO idle sleep; log the swapchain pre-transform
The 50us backoff in the FIFO_EMPTY path was added when the RSX thread was
measured spending 66% of its cycles in sched_yield on a machine starved for
cores: the affinity mask confined six SPU threads to four cores, and the
reservation path serialised everything behind a global lock, so a spinning RSX
took a core from threads that needed it.

Neither holds now, and the trade inverted with them. Measured after both were
fixed: 34% of eight cores busy, two to four threads runnable, five idle. Nothing
wants the core the sleep gives back, and the RSX sits on the frame's dependency
chain, so sleeping only delays it noticing the guest has produced work.

Also log the surface transform at swapchain creation. 30% of the frame is now in
check_present_status waiting on acquire_next_swapchain_image, which is not GPU
work -- a quarter-resolution run measured the same frame rate. Declaring IDENTITY
while the surface is rotated hands the rotation to the compositor, which can hold
images longer before releasing them for acquire. Logged rather than changed:
matching currentTransform means applying the rotation ourselves across the blit
and the overlay pass, and that is only worth doing if the two actually differ.
2026-08-11 10:02:54 -04:00
Megamouse a603fbba8c Update discord-rpc 2026-08-11 15:52:57 +02:00
jpolo1224 cb3670e2db Settings: restore upstream's GETLLAR busy-wait percentage
Dropped to 20 while the emulator was starved for cores, reasoning that a spinning
SPU steals a core from threads doing real work. Two things have changed
underneath that: the affinity mask no longer confines six SPU threads to four
cores, and the reservation path no longer serialises everything behind a global
lock. Measured after both, in game: 34% of eight cores busy, two to four threads
runnable, five idle, and no thread near saturation.

Tested at 100 and at 20 with no difference, which fits -- the wait is no longer on
the critical path, so how it waits does not matter. Upstream's value stands
rather than carrying a divergence that buys nothing.
2026-08-11 09:52:02 -04:00
jpolo1224 5f346b5e2c Settings: default the thread scheduler back to the OS
The affinity migration turned the scheduler on so the big.LITTLE mask would
apply, keeping SPU and RSX off the A510s that run at roughly 27% of prime-core
capacity. That reasoning holds for one thread per core and breaks down at six.

Measured in game on a Snapdragon 8 Gen 2, reading the masks the threads actually
carry:

    app cpuset (top-app)  0-7    Android grants every core
    SPU[0..5]             3-6    six threads, four cores
    rsx::thread           3-7

Six SPU threads sharing four cores get about two thirds of a core each, which is
worse than one thread owning an A510 outright, and it caps the emulator: the
device sat at 60% busy with cores 0-2 idle while frames were slow. Spider-Man:
Web of Shadows is visibly better at OS.

Worth being clear that the mask is ours and not Android's -- the app is in
top-app with all eight cores granted -- which is also why these devices are
reported to run better under native Linux, where no such policy is applied.

The other modes stay selectable for anyone whose device disagrees.
2026-08-11 09:33:32 -04:00
jpolo1224 1c2d44502c cellPad: keep pressure values in the buffer when press mode is off
A DualShock 3 sends pressure bytes in every packet. The press setting governs how
much of the buffer the game is told is valid, not whether the pad produced the
values, so clearing the area diverges from the hardware: a game that reads a
pressure byte without having asked for press mode gets 0 where it would see a
press on a console.

Spider-Man: Web of Shadows does exactly that for R2. Tracing both entry points
showed it never calls cellPadInfoPressMode or cellPadSetPressMode, so the setting
stays at 0, yet it reads the R2 pressure byte to decide whether the trigger is
held. R2 did nothing in that game while every other button worked, from the
controller and from the touch overlay and after remapping to a different physical
button, because the digital bit was delivered correctly the whole time and was
never what the game looked at.

len is unchanged, so a game that honours it sees what it saw before.
2026-08-11 09:22:58 -04:00
Megamouse db907a2586 TAR: Simplify result string creation 2026-08-11 10:55:02 +02:00
Megamouse b9cee7a3a9 Fix IsPathInsideDir arguments during file extraction 2026-08-11 10:55:02 +02:00
Megamouse d7c15851b4 Qt: run initial dialogs on the main event loop 2026-08-11 08:56:42 +02:00
Megamouse a723152dea Qt: make sure progress dialog stays hidden in the beginning 2026-08-11 08:56:42 +02:00
jpolo1224 ba5e4ebd66 SPU: stop an out-buffer verdict from spinning GETLLAR forever
The out-buffer check answers 'unlikely to be a loop', and that answer is not
free. It resets the spin count and leaves the busy-waiting switch at umax, so the
caller skips busy_wait, skips the sleep path, and returns immediately: the SPU
re-executes GETLLAR at full rate with no backoff. The spin count never reaches 4,
so the spin optimisation is never evaluated, and the 400ms fallback that would
force a sleep is never reached either. One SPU in that state holds a core flat
out, and the setting meant to control this has nothing to act on.

Spider-Man: Web of Shadows sits in that case. Its GETLLAR sites use an LSA in the
top 64K of local store, which is what the check looks for, and process_mfc_cmd
measured 55% of all CPU across the process while the game ran at 10-15fps.

Re-entering the same site with the same stack 32 times is itself the evidence
that it is a loop, whatever the LSA looks like. After that the verdict is dropped
and the normal spin detection decides between busy-waiting and sleeping. Any real
change of site or stack resets the count, so a genuine OUT buffer still gets the
original treatment.
2026-08-11 00:01:45 -04:00
jpolo1224 a873ae8250 Settings: let idle SPUs sleep instead of spinning on a reservation
SPU GETLLAR Busy Waiting Percentage defaults to 100 upstream, meaning always
busy-wait. That suits a desktop, where the SPU threads have cores of their own
and spinning costs nothing else. Here six of them share eight cores with the PPUs
and the RSX, so a spinning SPU takes a core from the threads doing the work.

Measured on Spider-Man: Web of Shadows: process_mfc_cmd accounted for 55% of all
CPU across the process, and making its inner loop cheaper did not move the frame
rate -- the loop just ran more iterations in the same wall clock. That is what
identified it as a spin rather than as work, after two rounds of optimising the
iteration itself.

20 still favours a short busy-wait, so a reservation that frees quickly is caught
without a scheduler round trip, and only a wait that history says is long goes to
sleep. A deliberate change in All Core Settings still wins, since core overrides
replay after this.
2026-08-10 23:56:15 -04:00
jpolo1224 e8c499056b SPU: memoise the GETLLAR out-buffer check on its inputs
Gating the check on getllar_spin_count was not enough. That counter is reset from
several other paths, so it is frequently zero and the callstack was still rebuilt
constantly: measured 19.6% of all CPU inclusive in dump_callstack_list, the
largest single item after process_mfc_cmd itself.

Key on the values the answer actually depends on instead -- pc, the stack pointer
and the link register -- and recompute only when one of them moves. Only the
innermost frame is ever used, so that is all the memo keeps.

A stale answer across an unrelated LS write is acceptable here. This decides only
whether the address looks like a caller's OUT buffer, on a heuristic whose own
comment calls it 'unlikely to be a loop'.
2026-08-10 23:52:23 -04:00
jpolo1224 88f4162883 SPU: evaluate the GETLLAR stack heuristic once per spin sequence
The out-buffer check in the GETLLAR spin detector rebuilt the callstack on every
iteration of a busy-wait loop. dump_callstack_list walks the stack and calls
is_exec_code for each candidate, which allocates a vector<bool> and scans for
branch targets, so the cost is large next to what it decides.

On a whole-process profile of Spider-Man: Web of Shadows those three came to
about 14% of all CPU -- more than the RSX thread spent on the frame -- because
the game's SPU code spins on GETLLAR with an LSA in the top 64K of local store,
which is exactly the case the check looks at.

Once per sequence is enough. pc, ch_mfc_cmd.lsa, gpr[1] and addr are all compared
against the previous iteration a few lines above and any change resets the
sequence, so the callstack cannot move underneath a spin.
2026-08-10 23:43:41 -04:00
jpolo1224 771a0dc74e RSX: back off to a sleep when the FIFO ring has gone quiet
sched_yield does not idle a core. With every core already busy it returns almost
immediately and the RSX thread takes it again, running flat out producing
nothing. A native profile of Web of Shadows put 66% of this thread's cycles in
sched_yield and its kernel path against 9% in run_FIFO, which the bucket profiler
reports as a busy RSX because the yield happens inside the fifo_decode scope.

That is not free even with an empty ring. The RSX affinity mask covers the whole
fast cluster while the SPU mask is that cluster minus the prime core, so any of
this that lands off the prime core is taken from the SPU threads, and those are
what the frame is actually waiting on at 61% of all CPU.

The spin still runs 64 times before sleeping, so a producer that is merely slow
is met without a scheduler round trip. Only a ring that has genuinely gone quiet
reaches the 50us sleep, which is far below the frame times where this matters.
Android only.
2026-08-10 23:37:48 -04:00
jpolo1224 1458d5f2d9 VK: end open occlusion queries in end_renderpass, for every caller
A query that begins inside a render pass instance has to end inside that same
instance. Ending the pass underneath an open one leaves it permanently
unavailable, and on Turnip it takes the device with it, reported later against
poke_query because that is the first call that waits on a result.

Queries do begin inside render passes here. VKDraw only lifts them out when
use_strict_query_scopes() is set, and that is wired to Strict Rendering Mode, a
user performance setting rather than a driver quirk, so it is off for almost
everyone.

Twenty-one call sites end a render pass and only one, in VKDraw, ever paired
itself with a cleanup. change_image_layout alone ends 41 passes a frame in Web of
Shadows, and any of them can land while a query is open, which is why fixing the
two sites in the query pool moved the device loss from one minute to nearly three
rather than removing it. Holding the invariant in end_renderpass covers all of
them, including any added later.

The VKDraw site now cleans up before the pass ends rather than after, which is
the order the spec asks for; its own call becomes a no-op.
2026-08-10 23:27:17 -04:00
jpolo1224 2cd341e257 Settings: purge the stale raw Relaxed ZCULL Sync override
The migration that turned relaxed ZCULL on recorded it twice: once in the curated
store and once as a raw core override. The migration that turned it back off only
corrected the curated field, so the two stores disagreed, and the override is the
one that reaches the core -- overrides re-push at the tail of applyTo, after the
curated store has written the setting.

The toggle therefore read OFF while config.yml read 'Relaxed ZCULL Sync: true' on
every boot, with no way to change it from the UI. That is not cosmetic: relaxed
sync is what allows queries to be read while still pending, which is the path
behind the 'Dubious query data pushed to cond render' warnings, and it also
selects emulated predication in the VK backend.
2026-08-10 23:17:53 -04:00
jpolo1224 fa97d01b0d VK: do not emulate predication where we disabled the extension ourselves
Emulated conditional rendering exists for hardware that never had the extension.
Turning the extension off as a driver workaround enabled it by accident, because
both are selected by the same test, and the two halves do not fit together:
begin_conditional_rendering returns early without building m_cond_render_buffer,
while the vertex shader still reads that buffer at offset 0. It gets a zeroed
scratch buffer, predicates every draw away, and the game renders black with audio
and overlays still running. The all-ones word that disables predication sits at
offset 4 and is never reached, since the fallback leaves hw_cond_active set.

Off on these drivers means occlusion results stop culling draws, which is the
trade the workaround already documents.

The vendor comes off the GPU rather than from get_driver_vendor(), whose cached
value is not assigned until later in the same function and would still hold the
previous device's.
2026-08-10 23:14:07 -04:00
jpolo1224 703d98ae9f VK: disable conditional rendering on Turnip as well as Adreno
The existing gate covered only the proprietary driver and said Turnip was left
alone until there was evidence about it. There is now.

Web of Shadows loses the Vulkan device about a minute into gameplay on Turnip 26
/ Adreno 740. The assertion names poke_query, which is the first call that reads
a result rather than the one at fault. Conditional rendering is the only place we
record vkCmdCopyQueryPoolResults with VK_QUERY_RESULT_WAIT_BIT, and that form
makes the GPU block until the query resolves, so a query that never resolves
hangs the device instead of the caller and the watchdog ends the session. The
same run logged 169 'Dubious query data pushed to cond render' warnings, which is
this code being handed queries that are still pending.

It is also the churn: the aggregation barriers closed 42 of the 91 render passes
in a measured frame, and ending a pass on a tiler costs a tile store and reload.

Both drivers now fall back to thread::begin_conditional_rendering, the path
desktop already takes wherever the extension is absent.
2026-08-10 23:10:32 -04:00
jpolo1224 cf2481fc11 VK: close open occlusion queries before ending the render pass
Fixing the device loss by ending the render pass before vkCmdCopyQueryPoolResults
introduced a hang in its place. A query that begins inside a render pass instance
has to end inside that same instance; ending the pass underneath an open one
leaves it permanently unavailable, so get_query_result spins on poke_query with
no way out and the RSX thread stops.

Nothing reported it. The submit-time ensure() only checks that the query was
closed, and end_occlusion_query closes it a moment later, so the assert passes
while the result never arrives. The stall detector runs from do_local_task in the
FIFO loop, which the spin has already left, so the profiler charged the wait to
FIFO decode and the frame read as CPU-bound -- 93% in a bucket that was really
the thread sitting in sched_yield. Web of Shadows locked up this way after
reaching gameplay, audio and vblank still running.

Both sites that end a pass from the query path now close an open query first,
which keeps begin and end within one pass. The query is cut short, as it is
anywhere do_query_cleanup is used.

The wait itself is now bounded as well. It warns at one second and abandons at
three, using whatever the query holds: wrong culling for a frame is a better
failure than a thread that never returns, and the log names the cause.
2026-08-10 23:01:36 -04:00
jpolo1224 8041edf5bc RSX: publish GET only when it has advanced
The drain fix made every path that can idle or block publish GET immediately,
which is required for correctness: a producer waiting on ring space needs to see
the progress we made before we stopped consuming.

It publishes far more often than that requires. The empty and busy cases return
straight to the run loop, so a ring that has gone quiet re-enters them once per
iteration with GET unmoved. Web of Shadows measured 137000 loop iterations per
frame against 46000 method dispatches; the remaining 91000 were republishing a
value the guest already had.

GET shares a 64-byte line with put, which the guest PPU writes from another
cluster, so each of those is a coherence miss taken against the thread feeding
the ring. The cost lands on the producer rather than on the RSX, which is why it
presented as a freeze with sound still playing: the PPU stalls on the contended
line while threads that never touch it keep running.

GET is ours to write, so tracking the last published value and skipping an
unchanged store keeps the guarantee -- progress is still announced exactly once
after the last advance -- without the repeats.
2026-08-10 22:50:14 -04:00
jpolo1224 d909537f3f Stop copying query results from inside a render pass
vkCmdCopyQueryPoolResults must be recorded outside a render pass instance. This
recorded it inside one, with a comment saying we are technically supposed to stop
the pass first but that it does not matter on IMR hardware. It is not a
technicality -- inside a pass it is undefined behaviour, and a desktop GPU
tolerating it says nothing about a tiler.

This device lost the Vulkan device over it. The fault surfaced later, in
poke_query, because that is the first call that waits on a GPU result, so it read
as the query READ being at fault when the damage was done at record time. Only
the RSX thread died, so the process kept running with audio and vblank alive and
it presented as a hard freeze rather than a crash. Verified gone: zero device
losses on a run that previously died within a minute.

The pass is ended only when one is actually open, on a path that already stalls
for a GPU result, so the flush the upstream comment worried about is paid where
we were blocking anyway -- and disabling occlusion queries is not the
alternative, measured here at 80ms frames with broken visuals.
2026-08-10 22:34:20 -04:00
jpolo1224 0cbab0c3ae Join the GPU and CPU pass tables on the same ordinal, and reach external storage
The two by-pass tables were joined on counters that reset at different points.
tick_frame runs from on_frame_end, before flip; the GPU timer rotates its slot at
the top of flip and then drops every non-frame region on the fresh slot, which is
flip's own overlay and calibration passes -- and those still incremented the CPU
counter. So the CPU ordinal ran ahead by the number of present-path passes and
the two tables described different passes. A whole anomaly came out of that: a
pass whose GPU cost was joined to a neighbour's workload read as 36x the per-draw
cost of its peers. The comment claiming both reset on the same boundary was
wrong. Reset where the GPU slot actually rotates instead.

Also adds a Storage Access Framework route to the package installer. The in-app
browser walks java.io.File, which only reaches storage this process can open by
path, so a .pkg on a USB-OTG drive or an SD card was unreachable and had to be
copied to internal storage first. Packages are handed over as the descriptor SAF
already returned -- the native side takes a raw fd, so nothing is copied and a
4 GB package costs no extra space; licences are 16 bytes and their installer
wants a real file, so those alone are staged.
2026-08-10 22:28:33 -04:00
jpolo1224 3338eedfcd Bound the PPU compile serialisation so a dead worker cannot strand it
The low-memory serialisation added three days ago holds a std::mutex across the
LLVM compile itself. A worker that hits LLVM's fatal handler leaves through
pthread_exit, and bionic unwinds nothing on that path, so the mutex stays locked
by a thread that no longer exists and every remaining worker waits on it for the
rest of the session. Memory only falls as modules accumulate, so the tight-memory
branch is likeliest late in a run -- which is why it reads as the PPU cache
getting stuck at the very end, and why dropping to the interpreter avoids it.

Reported as Saint Seiya: The Sanctuary never finishing its module cache. A claim
taken by compare-and-swap and waited on with a timeout costs a stranded claim a
wait rather than the session; the memory back-pressure either side of it is
unchanged. Third time this fork has been bitten by an unbounded wait around a
thread bionic can kill without unwinding.
2026-08-10 22:13:01 -04:00
jpolo1224 062cde277a Stop the save-data list aborting where there is no media backend
overlay_audio.cpp already accounts for a platform with no video source; the same
ensure() was left in overlay_video.cpp. Android's make_video_source returns
nullptr, and overlay_save_dialog builds a video_view for EVERY entry on all three
of its paths, so opening a save list aborted as soon as there was one save to
draw. It presents as the save menu never opening -- reported against Ratchet &
Clank: Tools of Destruction and Devil May Cry 4, and against Web of Shadows,
which stalls only once a save exists to be listed. Bundling the overlay icons
was necessary but not sufficient: the dialog still could not survive drawing.

The still image is what an entry needs; the animated ICON1.PAM is the part no
backend here can supply. Also dumps SPU thread pc and block hash alongside the
PPU dump when frames stop, which is what named the SPURS kernels as idle rather
than spinning in guest code.
2026-08-10 22:09:59 -04:00
jpolo1224 7b0f1cd6de Ship the overlay icons the native UI has been drawing without
overlay_controls.cpp loads a fixed set of PNGs -- button glyphs, save.png,
new.png, spinner -- through fs::get_config_dir() + Icons/ui/. Desktop ships them
beside the binary; nothing put them on Android, so every load failed and the log
said so on each boot. The visible cost was cellSaveData's list: it is a native
overlay that draws its rows with save.png/new.png, so the load-save menu a game
opens never appeared. Reported against Ratchet and Clank: Tools of Destruction
and Devil May Cry 4, both fine on emulators that ship the icons.

Bundled from bin/Icons/ui and staged into config/Icons/ui once, revision-guarded,
before the core can draw its first overlay.
2026-08-10 18:36:17 -04:00
jpolo1224 05842b3115 Retranslate every language from the current English map
The nineteen translation files were ARMSX2-era: about nine hundred of their keys
still existed and showed the old PS2 wording -- worse than the English fallback,
which is at least right -- and roughly eight hundred current keys had no
translation at all. Regenerated all nineteen from the 1041-key map, batch plus
per-line retry, with every %s/%d checked against the source so no broken format
string ships. A string that would not translate is omitted and falls back to
English rather than shipping wrong.
2026-08-10 18:36:17 -04:00
jpolo1224 d6235c5802 Remove the PS2 leftovers a PS3 emulator was still carrying
The memory-card and PNACH patch screens were PS2 concepts with no PS3 counterpart
and no caller left -- the drawer had already been cleaned, so they were dead code
holding dead strings. The PNACH downloader went with its only consumer, and the
PCSX2-Android.ini seed could never exist under this package. The session log now
announces ARMSX3_INIT instead of PCSX2_INIT, which had every bug report opening
with the name of a different emulator.

The English map drops 46 PS2 strings and 619 orphans nothing references (1705 ->
1041 keys), rewords the four live strings that still said memory card, and renames
about.pcsx2.* to about.rpcs3.* to match what they already said.
2026-08-10 18:36:17 -04:00
jpolo1224 94b12dc216 Release 0.4.2 2026-08-10 16:20:10 -04:00
jpolo1224 ac7639457f Say when the game has stopped drawing instead of leaving the last frame up
A guest that stops progressing presents nothing further, so whatever was drawn
last stays on screen for good. When that frame held the boot progress bar it read
as stuck compiling at 1s remaining, and it looked identical across five unrelated
faults -- it sent every report of them to the wrong place, including this week's.
Nothing contradicted it either, since the emulator has not crashed and logs no
error.

Reports once, to the log and to the screen, after thirty seconds with no frame
and nothing claiming to be in progress. The progress text is what separates
working quietly from stopped: a shader or PPU compile presents no frames for
minutes and holds a dialog saying so. Drawing it needs the native UI flip,
because the guest is not flipping -- which is the point.
2026-08-10 16:20:10 -04:00
jpolo1224 84db19dc3e Publish GET before the RSX stops consuming
GET goes out on a bounded lag, every eighth packet, to keep a cross-cluster
coherence miss off the per-packet path. That is only sound while more packets are
coming to flush it, and the paths that can block were given a forced publish for
exactly that reason -- but the one where the ring runs dry was not, and it is the
one where nothing further will ever flush it.

The guest reads GET to see how far the RSX has consumed. Draining the ring left
it up to seven packets behind with no more packets to publish, so the guest waited
on progress that had already been made and never announced.

It presents as a boot or a load that hangs with the RSX perfectly healthy and idle,
every guest thread in a legitimate wait, and no error anywhere: bisected to this
across six rounds after five wrong theories, because nothing is broken at the point
it stops. Only bites when the packet count is not a multiple of eight as the ring
drains, which is why it was game- and timing-dependent and why the same title could
boot yesterday and hang today.

Publishes on both paths that leave the consume loop. The lag stays.
2026-08-10 16:14:18 -04:00
jpolo1224 5eb3d64ed0 Name where each guest thread is parked when frames stop
The RSX-side stall report says what the RSX is doing, which on every hang chased
so far has been idling while the guest waits -- and nothing said which guest
thread or what it was in. The syscall stats name the syscall without the caller,
and a thread that has not started reads from /proc exactly like one that is
blocked.

One line per PPU thread with its name, state, PC and current function, on the
same condition and cadence as the RSX report. Reads the id map unlocked on
purpose: this runs on the RSX thread, and taking that lock to diagnose a hang
would add the kind of dependency being diagnosed.
2026-08-10 15:49:20 -04:00
jpolo1224 d82f1df96b Report RSX stalls and vblank liveness without needing a frame
The profiler arms and reports only from on_frame_end, and dumps once 300 frames
have accumulated, so a boot that hangs before presenting left it switched off and
silent however the setting was set -- the one case where what the RSX thread is
looping in is the whole question. Armed and polled from do_local_task as well,
which the FIFO loop reaches whether or not frames advance.

The vblank thread is the only source of the interrupt gcm waits on, and said
nothing about being alive, blocked or gone. A heartbeat and an exit reason
separate those, which are three different faults that look identical from
outside: on a Demon's Souls boot it delivered about 120 vblanks and then parked
in the send path with the queue undrained.
2026-08-10 15:41:38 -04:00
jpolo1224 697cacd854 Bound the waits on an SPU compile claim
Waiting on the claim was untimed, so a waiter that missed the owner's transition
waited for the rest of the session, and the duplicate waiter could only leave on
the failure state -- an owner that published state 2 without publishing a
function left it waiting on something that was never coming.

SPURS brings all of its kernels to the same block at once, so this is four
threads at a time, and the PPU then blocks on SPUs that never answer. Measured
during one: the PPU thread took no CPU at all across eleven minutes while the
SPU threads churned two-to-one system time.

Both waits are bounded now and the duplicate leaves when the owner has finished
and published nothing.
2026-08-10 15:12:16 -04:00
jpolo1224 ab3335b8d6 Make the auto-save options and save-state import do what they say
Auto-save on exit, auto-load on boot and the interval auto-save were ARMSX2 shims
returning false that were never ported, so all three toggles persisted and read
back while doing nothing -- the interval job woke on schedule to call a function
that always failed. They now use a reserved slot above the ten the picker shows,
reusing the numbered-slot path rather than growing a second mechanism.

Import treated getGamePathSlot as a file path, but it answers with the title id:
File(id).exists() was false for every slot, so the first OCCUPIED slot read as
free and the destination resolved against the process working directory. It
copied the file nowhere useful and reported the slot it had not written.
Occupancy now comes from the core and the destination from the real path.

Also says how large a state is before the storage bill arrives, and stops the
interval description promising a pause when a PS3 save is a stop and a reload.
2026-08-10 13:20:38 -04:00
RipleyTom 2f4034590f sys_net: fix possible event gap in recvfrom 2026-08-10 20:03:56 +03:00
RipleyTom 7b6a8cc2d6 sys_net: more fixes
Add parameter checks to sys_net_infoctl
Fix P2P getpeername() fatal
Fix possible deadlock in tcp_timeout_monitor
Remove vport assert from poll
Add upgrade path for new P2PS state in savestates
Fix P2PS close_stream() not waking threads
Fix possible deadlock in P2PS connect()
Ensure RST is sent on unhandled packets
2026-08-10 20:03:56 +03:00
RipleyTom 6df35891ad sys_net: fixes and improvements
Add thread lock for p2p sockets(used for poll/select to avoid event gap)
Fix poll/select returning EINTR if no sockets were polled
Implement sys_net_infoctl cmd 6(sys_net_get_sockinfo)
Cleanup sys_net_infoctl cmd 9 code
Add SYS_NET_STATE_* values to header
Rewrite P2P and P2PS poll/select implementations
Add extra P2PS state for disconnected and report it as readable with 0 bytes(EOF)
Fix P2PS sockets connecting without being bound first missing hashmap insert
Add missing error check in sceNpSignalingActivateConnection
2026-08-10 20:03:56 +03:00
jpolo1224 99b6b47ee1 Look for slot thumbnails where the core actually wrote them
The reader rooted the path at systemDirPosix(), which is null unless a data
folder was explicitly picked, so on a default install it returned before it ever
looked and every slot drew as a blank tile with the thumbnail sitting on disk
beside the state it belongs to. Falls back to getExternalFilesDir, which is what
fs::get_config_dir() resolves to on that install and where the writer put it --
the same fallback inputProfilesDir() makes for the same reason.
2026-08-10 13:03:07 -04:00
jpolo1224 6255ce5840 Run posted main-thread callbacks off the caller's thread
CallFromMainThread without a wake_up is a post: upstream hands the callback to
the GUI thread and returns. This ran it inline instead, under whatever locks the
caller held.

lv2_obj::sleep_unlocked posts one while holding lv2_obj::g_mutex, which is what
the comment on that call site is about. The callback is FinalizeRunRequest, the
wake for a restored savestate, so it took g_mutex against itself and every thread
stopped there: the log reaches Final Thread and goes quiet with the SPUs spinning
and the progress overlay frozen on its last figure. It took out loading a state
and saving one alike, a save being a stop and a restore.

Callers passing wake_up are waiting on completion and still run inline.
2026-08-10 12:52:51 -04:00
jpolo1224 87b0992fdd Revert keying the SPU cache on savestate-compatible mode
The hash it changed is one of several computed over the same function data, and
only this one moved, so the cache rebuild it forced ran through a path whose
other sites disagreed. Loading a state then wedged in Building SPU cache with
nothing compiling.

Reuse of blocks across a change of the setting is still wrong, but it is an
upstream behaviour that predates this and is better addressed by invalidating
the object cache once when the mode changes than by moving one hash out from
under the others.
2026-08-10 12:40:22 -04:00
jpolo1224 2a0c04ade7 Stop treating an already-registered game as a boot failure
already_added reports that the title was already in games.yml, which is the
normal case for anything booted once before, and RPCS3's own front-end passes it
through for that reason. The bridge failed every result that was not NoErrors,
so the boot was abandoned and the user was returned to the library with "Game
failed to start: AlreadyAdded".
2026-08-10 12:38:35 -04:00
jpolo1224 5b8ff01fe2 Route the savestate-compatible setting to the core
The bridge translates each section it knows and returns false for the rest, and
there was no Savestate case, so the write was dropped on the floor. The setting
could not be turned on at all -- not by the default, not from the settings row --
and savestates failed to lock the SPUs while telling the user to enable exactly
the option that was being discarded.
2026-08-10 12:23:55 -04:00
jpolo1224 51d465f193 Key the SPU cache on savestate-compatible mode
The setting changes the code generated for blocking channel reads -- the GPRs
are stored rather than the thread being marked unsavable -- but the cache key
was a hash of the guest code alone, and the compiled object is cached under it.
A block built in one mode was therefore reused unchanged in the other, so
turning the setting on left the old unsavable blocks in place and savestates
went on failing to lock the SPUs with a message telling the user to enable a
setting they had already enabled.

Mixed in only when set, so caches built in the default mode stay valid.
2026-08-10 12:17:53 -04:00
jpolo1224 6cd5b8b986 Turn save states on by default
A save that fails with "missing SPU setting" reads as broken rather than as a
setting waiting to be found, and the setting is not one a player would think to
look for. Upstream defaults it off to protect SPU performance; here the feature
not working at all is the worse trade.

Costs are unchanged and still stated on the switch: it slows the SPUs while it
is on, and a PS3 state runs 500MB to 3GB. Turning it off restores upstream
behaviour and gives the performance back, and that choice is now respected on
every boot rather than overwritten.

Release 0.4.1.
2026-08-10 11:51:09 -04:00
jpolo1224 44f62f310c Let the user turn save states on
Save states could not be taken at all. Saving has to stop every SPU somewhere it
can be serialised from, which is what Compatible Savestate Mode does, and this
port wrote that setting to false on every boot -- so the save failed with
"missing SPU setting" no matter what the user did, and nothing on screen
connected the two.

The reasoning was sound: the mode costs SPU performance, and a PS3 state runs
500MB to 3GB, so a few saves fill a phone. Both of those are costs to disclose,
not reasons to decide for someone. It is now a setting, still off by default, so
nobody pays for a feature they did not ask for and installs from the window when
it was pushed as true are corrected by the same write.

Placed with the SPU rows rather than under a savestate heading, because that is
where the cost lands, and the description says what both costs are before the
switch is touched.
2026-08-10 11:47:41 -04:00
jpolo1224 431b6d0925 Stop a recovered LLVM fatal error from wedging the SPU JIT
run_recoverable_llvm runs code generation on a disposable thread and terminates
it through pthread_exit when LLVM invokes its fatal error handler. bionic does
not force-unwind C++ frames on pthread_exit, so the lock MCJIT holds over the
execution engine is never released and stays held by a thread that no longer
exists.

Every entry point into the engine takes that lock, so the next compile hangs and
so does teardown. One recovered error, and the emulator is finished until it is
killed -- and the error is recovered, which is the point: it is meant to be
survivable.

Their Kotlin log-channel screen is left out; this port has its own.

From MaxsTechReview in PS3Native.
2026-08-10 11:44:11 -04:00
jpolo1224 4a73773cee Bound two unchecked sizes reached from file contents
Two places compute a size from values a file supplies and use it without
checking it is possible.

A SELF or SCE header gives the metadata offset and the header size, and the
buffer between them is sized by subtracting one from the other. Both are
unsigned, so a truncated or malformed dump that puts the offset past the header
end underflows into a near-SIZE_MAX allocation, which fails as an out-of-memory
rather than as the bad file it is. Reject the layout instead.

Texture uploads take the mip levels the guest describes and write them into an
image built from the destination's own dimensions, which can hold fewer. Drop
the levels that do not fit rather than writing past what was allocated.

From MaxsTechReview in PS3Native.
2026-08-10 11:43:05 -04:00
Zulux91 e16f0fcd3d Sample thread CPU time by tid on Android
get_cycles passes the pthread handle to pthread_getcpuclockid, which glibc
answers with an error for a thread that has already exited -- the else branch
below returns the last known value for exactly that case. bionic instead looks
the handle up in its list of live threads and aborts the process when it is not
there.

m_thread is never cleared when a thread ends and the performance overlay samples
every PPU, SPU and RSX thread on a timer, so one finished thread is enough to
take the emulator down with it. Latent here rather than absent: it needs the
overlay on and a thread to have gone.

Record the kernel tid at initialize and build the per-thread clock id from it
the way bionic does once its own lookup succeeds, so clock_gettime simply fails
for a dead thread, which is what the surrounding code already expects. Cleared
at finalize so a thread stops being sampled before it goes away. Other platforms
keep the original path.

Found and fixed by Zulux91 in PS3Native.
2026-08-10 11:42:14 -04:00
jpolo1224 6463d010e9 Ask the game for its content id when installing its licence
A licence installed from a locked game's own menu reported success and left the
game locked. The install writes the file into exdata under its own name, because
a RAP's name is conventionally the content id it unlocks, and that convention is
the whole lookup: the core opens exdata/<content id>.rap and nothing else. A file
saved as "license(1).rap", renamed, or tidied up on the way over therefore lands
where nothing will look for it, and the only feedback is the game asking again.

Where the game is known, ask it. The native path decrypts the EBOOT's
supplemental header to read the content id, which works on a locked game because
that header is not what the licence protects, and names the file correctly
whatever the user's copy is called. Falls back to the name when there is no game
to ask or the header cannot be read.

The package screen keeps the name-based path: a licence installed on its own has
no game to resolve against.
2026-08-10 11:36:42 -04:00
Megamouse eaef23d43a unself: fix overflow checks 2026-08-10 17:35:26 +02:00
Megamouse 8cde4b2153 unself: fix more potential OOB 2026-08-10 17:35:26 +02:00
Megamouse ed3af84437 unself: add some sanity checks and optimize uncompress a bit 2026-08-10 17:35:26 +02:00
Megamouse 81b85e55e4 unself: cache buffer during decrypt 2026-08-10 17:35:26 +02:00
Megamouse 9af11f5cf1 unself: add some OOB checks 2026-08-10 17:35:26 +02:00
Megamouse d6d5c60823 ISO: mark archive as invalid on error and add sanity checks 2026-08-10 14:45:00 +02:00
Megamouse 0230580a88 ISO: exit loop at end of file 2026-08-10 14:45:00 +02:00
Megamouse ae98583ed2 ISO: ensure filename size during parsing 2026-08-10 14:45:00 +02:00
Megamouse 628ea5ec15 ISO: ensure we're not trying to install files outside of their parent 2026-08-10 14:45:00 +02:00
Megamouse 400d9a1c24 ISO: fix potential overflow 2026-08-10 14:45:00 +02:00
Zulux91 4101367b2d Verify downloaded patches and match the engine's wildcard serial
Three things in the patch download path, all of them things desktop
RPCS3 already does.

The download URL had the patch schema version written into it as 1.2.
That is right today, but patch_engine::load rejects any file whose
Version header doesn't match the core's patch_engine_version, so the day
upstream bumps that constant every download starts failing to parse. The
core now hands the version out through patchEngineVersion() and the URL
is built from it. I also check the version the server echoes back, which
turns a several megabyte download into an early error instead of a
parser complaint.

Nothing verified the sha256 the server sends alongside the patch text.
Desktop checks it before it writes anything (patch_manager_dialog::
handle_json). Patches are writes into the guest executable, and
move_file/hide_file patches reach the emulator's own filesystem, so I'd
rather not import bytes that aren't what the server hashed. Mismatches
get their own message rather than being reported as a parse failure.

Last, the wildcard serial. patch_key::all is spelled "All", and
patchSetEnabled compared against a lowercase "all", so a patch carrying
a wildcard entry never had that entry written.

I first "fixed" the same typo in patchesList and let per-game lists match
the wildcard too. Ooops. Turns out that is not a typo doing nothing, it
is a typo doing the right thing by accident: wildcard patches are keyed
by SPU or PPU hash and leave the serial as "All" because the hash is the
filter, so they belong to no single game. There are 17 in the database,
and on device Skate 3 cheerfully offered me a pile of LittleBigPlanet
MLAA patches, where toggling one writes the shared entry and changes
every other game as well. Desktop shows them once under an "All titles"
node, so per-game lists stay serial-only here and the global list is the
equivalent. Only patchSetEnabled and the enabled-state read get the
spelling fix.
2026-08-10 05:48:03 -05:00
Megamouse 58ef670992 Add file path validations during extraction 2026-08-10 12:35:49 +02:00
Megamouse de13ae4753 Initialize emu callbacks before initializing the emulator 2026-08-10 12:35:49 +02:00
Zulux91 466d85d5b9 Report PS3 patch state per game and say when it takes effect
Two things in the patch list reported something the game wasn't getting.

First, patchesList marked a patch as enabled if any entry under its hash
was enabled, whichever serial that entry belonged to. patchSetEnabled
writes per serial, so a patch I switched on from one game's list showed
as on in every other game that patch covers. It now scans only the
requested serial, plus RPCS3's "all" wildcard, which really does apply to
the game being listed.

Second, the patch engine builds its map once while the game loads, then
writes the patches into each module as that module loads. Toggling a
patch only rewrites patch_config.yml, so nothing happens in a game that
is already running. The in-game tab never said so, which makes the switch
look broken. It says so now, above the list.
2026-08-10 05:01:54 -05:00
jpolo1224 6961adf597 Split the DMA and blit engine handlers
Those two are 7.4 ms a frame in Arkham City, a fifth of it, and the GPU side of
the same work is 0.76 ms, so it is host work. Each handler does several unrelated
things and nothing separates them: a read barrier that can force a readback, the
memory copy the transfer exists to perform, and in the blit engine a software
scale through ffmpeg for the cases the GPU path does not take.

Scope the three. The scale is scoped inside convert_scale_image rather than at
its four call sites in the blit engine, and only the RSX thread is ever reported,
so calls from elsewhere cost nothing to cover.

Also let a scope be closed early, so a region ending part-way through a function
does not need a block introduced purely to place a brace.
2026-08-10 05:32:03 -04:00
jpolo1224 d7f2eba643 Look method names up by the key the table actually uses
The name table is keyed by register index, and both method reports passed the
byte offset. A lookup therefore matched whichever unrelated method happened to
have that value as its enum, so the costliest entry in Arkham City came out as
NV4097_SET_CONTEXT_DMA_VERTEX_B, which has no handler and cannot cost anything.
It was NV406E_SEMAPHORE_ACQUIRE: the RSX waiting for the guest to signal, which
is the one entry in that list that is supposed to block and the one that should
not be optimised.

A wrong name is worse than none here. The hex fallback was right the whole time
and is left as the byte offset, which is what a reader looks up.
2026-08-10 05:28:11 -04:00
jpolo1224 b209907dc1 Rank method handlers by cost instead of by volume
The handler bodies are 60% of the RSX thread in Arkham City and the dispatch
machinery around them is 3.5%, so the question is which handlers. The method
histogram cannot answer it: it counts calls, and the busiest method may be a
register write while a rare one does the work.

Keep the interval the dispatch site already measures. It brackets the call with
two counter reads to fill the method_call bucket and then throws the difference
away; billing it to the method's slot as well costs one add.

Inclusive of whatever the handler calls into, including scopes that charge
themselves elsewhere. For ranking handlers that is the useful reading, and the
per-bucket totals stay exclusive as they were.
2026-08-10 05:22:13 -04:00
jpolo1224 b4d63d6c0a Split method handler bodies out of FIFO decode
Arkham City spends 38.5 ms a frame in FIFO decode, 58% of the RSX thread, at
164 ns a dispatch. Sonic manages 45 ns on the same loop, the same decode and the
same counters, so the difference is in what the handlers do rather than in the
dispatch. Nothing separates the two: fifo_decode encloses the whole loop, so it
holds every handler body as well as the machinery around them.

The two handlers already scoped, transform program and transform constant,
measure 0.02 and 0.06 ms here, which rules them out and leaves the rest of the
mix unaccounted for.

Wrap the handler call. This is the only per-dispatch scope in the profiler and an
earlier attempt at one measured mostly itself; it is affordable here because it
brackets a call rather than a loop iteration, and handlers carrying their own
scope still attribute inward. It costs a few percent of the bucket it splits, so
the split is the number to read, not the total.
2026-08-10 05:17:54 -04:00
jpolo1224 d182338669 Re-bind the profiler when the RSX thread changes
Booting a second game without restarting the app builds a new RSX thread.
set_enabled is the only thing that binds the profiler to a thread, and it
early-returns when the setting has not changed, so it stayed bound to the
previous game's thread. Every scope then failed its owner check, nothing
switched buckets, and the whole window was charged to whichever bucket happened
to be current.

That prints as "FIFO decode 100.0%", which is indistinguishable from a genuine
finding about a command-bound title, and was briefly read as one.

Notice the change per frame and re-bind, dropping the accumulated window and the
per-pass counters: they belong to a thread that is gone, and keeping them would
blend two games into one report.
2026-08-10 04:59:03 -04:00
Megamouse f48ca59235 unedat: fix division by 0 2026-08-10 10:53:30 +02:00
Megamouse 804e06356b unedat: fix some data types to prevent overflow 2026-08-10 10:53:30 +02:00
Megamouse 70e3e15e2f unedat: Fix more potential OOB reads 2026-08-10 10:53:30 +02:00
Megamouse 852407b8cb unedat: don't use raw pointers everywhere and use more const 2026-08-10 10:53:30 +02:00
jpolo1224 171dcfc4ad Release 0.4 2026-08-10 04:37:59 -04:00
jpolo1224 776d8c65fc Back up the data this emulator actually has
The include list came over from ARMSX2 and names sstates, memcards, gamesettings,
cheats and snaps. None of those exist here, so a backup collected a few kilobytes
of controller profiles, reported success, and left every save behind. Nothing
warned: skipping an absent folder silently is right for an optional one and wrong
for a list aimed at a different emulator.

Name RPCS3's paths instead. The part that matters is config/dev_hdd0/home, which
holds save data, trophies and licences, and is the only thing in here that cannot
be rebuilt or re-downloaded. Save states, input configs and patches come along.

Installed titles are left out on purpose, along with firmware and dev_hdd1: a PKG
reinstalls and a PUP reinstalls, a save does not, and that is the line this list
is drawn on. Including them would have taken the archive from tens of megabytes
to nearly five hundred on the device this was sized against.

The description on the screen said memory cards and artwork too, so the one place
a user could have noticed agreed with the bug.
2026-08-10 04:34:41 -04:00
jpolo1224 4440eb30b9 Count occlusion queries per pass
Everything measured so far describes what a draw contains: vertices, pixels,
shader length, subdraws, barriers. By all of them pass six should be the
cheapest of the expensive passes, and it is the dearest by a factor of seven.

An occlusion query is none of those things. On a tiler it makes the visibility
stream resolve, it costs the same whatever the framebuffer size, and no counter
here would show it. That matches every property this pass has: indifferent to a
sixteen fold cut in pixels, indifferent to tiling being switched off, no
barriers, one subdraw per draw, shorter shaders than the passes it dwarfs.

ZCULL is active, and emit_geometry opens a query whenever the command buffer
carries the occlusion flag. Count them where they open.
2026-08-10 04:21:33 -04:00
Zulux91 1e320e3de2 Fix .rap licence installing and surface licence-locked games
Installing a .rap never worked at all. The package screen routed licences to
installKey, whose RAP branch works out the content id by decrypting the game's
EBOOT, so it needs a game path, and the only caller passed an empty one. Every
attempt died at "Failed to fetch NPDRM of SELF". A RAP's filename is the content
id it unlocks, which is why RPCS3 desktop's InstallFileInExData simply copies the
file into exdata. That is what this does now, lower case extension included,
because unself.cpp searches for it that way.

Picking a game together with its licence could not work either. The installer
routed on file count rather than file kind, so any multi file selection went to
installSplitPkg, whose first act is to reject anything that is not a .pkg part.
Multiple selection has been allowed since split packages landed, so the obvious
thing to do was the one thing guaranteed to fail. The selection is split by kind
now, packages first, since a licence unlocks content the package has to have
written already.

Both failures showed the same generic "Install failed. The file may be encrypted,
incomplete or not a PS3 package", which reads as a bad file rather than a bug in
the app. The reason the native side already reported now reaches the screen.

A licence-locked title also looked like any other until it refused to boot. The
core works that flag out by attempting decrypt_self on the EBOOT, but the library
never asked it. The scan asks now, and a locked game gets a badge on its cover, an
Install licence entry in its context menu, and a prompt instead of a doomed boot
from every launch path: the library cards, the context menu, the controller, and
the settings screen's Play button.

Boot failures were silent besides. Rpcs3Bridge.boot threw away BootGame's return
code and MainActivityRuntime dropped runVMThread's result, so a failed boot was
indistinguishable from a game that started and exited immediately. Both are
reported now, which is how I found the licence problem in the first place.

External intents and launcher shortcuts are not covered, because externalGameInfo
builds a fresh GameInfo where locked defaults to false. Those still fall back to
the boot failure message.
2026-08-10 04:20:21 -04:00
Zulux91 de34f7f173 Remove a title's shader and PPU cache when uninstalling it
Uninstalling only ever removed dev_hdd0/game/<TITLEID>, so the title's compiled
code and shader cache stayed on disk forever. On my device that was between 7 and
58 MB per title, and one of those caches belonged to a game I had already removed.

I made it a checkbox on the existing confirmation rather than doing it silently,
defaulted on, which is how RPCS3 desktop's own remove dialog treats caches. The
row is hidden when there is no cache, and it shows the measured size so you can
see what you are freeing. The size is measured off the main thread because a cache
directory holds hundreds of files and this runs while the dialog is opening.

The cache goes only after the native uninstall reports success, since dropping the
cache for a title that is still installed would just cost a recompile. The title
id is validated before the recursive delete: it comes from a directory listing,
but a path separator or a dot dot in it would resolve outside the per title
folder, so anything that is not a single plain segment is refused.

Save data, trophies and licences are deliberately left alone. Those belong to the
user rather than to the install, and desktop does not offer to remove them either.
2026-08-10 04:16:00 -04:00
jpolo1224 e5673c43ea Give back the extra frame in flight when memory is tight
A session ended in a fatal VK_ERROR_OUT_OF_DEVICE_MEMORY, the first in any log
here. On this GPU that is system memory, and the device had two gigabytes free
of seven with the emulator holding most of the rest.

Two frames in flight is what makes the CPU and the GPU overlap, and it is also a
second frame's worth of resources alive before anything retires them. That trade
is worth making at rest and not worth making into a crash on a handheld sharing
memory with everything else.

Fall back to the single frame this used to run with when the memory load is
above low. Slower, and slower is recoverable.
2026-08-10 04:06:41 -04:00
jpolo1224 675b2e679f Count the draws the GPU receives, not the ones the guest issued
Shader length settled that pass six is not the game's workload: it has the
shortest shaders of the expensive passes, a quarter of the vertices of a pass
that costs a seventh as much, no barriers, and no reaction to resolution or to
tiling being switched off. Every quantity measured so far says it should be
cheap, and it takes nine milliseconds.

The draw count is the one that has been lying. It counts clauses, and a clause
is expanded over its subranges, so a single entry can become thousands of draws.
Batching them through VK_EXT_multi_draw, which this device does support, saves
our command overhead and changes nothing about how many the GPU processes.

Count them at every submission site. Thousands of tiny draws at a fixed cost
each is the last shape that fits, and nothing else measured would reveal it.
2026-08-10 04:02:39 -04:00
jpolo1224 2b036458ee Measure shader complexity per pass
Pass six costs about 26 times what pass eight does per vertex: 123 draws and 68
thousand vertices for 9.15 ms against 532 draws and 253 thousand vertices for
1.26 ms. It has no barriers, does not care about resolution, and does not change
when TU_DEBUG=sysmem takes tiling and binning out of the picture entirely. The
only thing left that behaves that way is the shader.

Record vertex and fragment ucode length per pass. This decides whether there is
a bug here at all, which nothing measured so far can: shaders genuinely that
much longer are the game's own workload and there is nothing to fix, while
comparable ones mean something is happening to those draws that should not be.
2026-08-10 03:57:18 -04:00
jpolo1224 0cc115af48 Log the driver options actually applied
The previous commit read driver_env.txt before the log file was opened, so the
one thing worth knowing -- whether the option was applied -- was written into a
listener that did not exist yet and then thrown away when the log rotated.

Move the read to just after the log file is created and report each option by
reading it back rather than echoing what was meant to be set. There is no other
honest confirmation available: /proc/<pid>/environ is the snapshot taken at exec
and never reflects a runtime setenv, and Mesa's own logging goes to stderr,
which Android discards. Still long before any Vulkan instance exists, which is
the only ordering Mesa cares about.
2026-08-10 03:52:23 -04:00
jpolo1224 a2dd09376b Let Mesa driver options be set without a rebuild
This device needs Turnip; the stock Adreno driver does not render the game at
all. Turnip is steered by environment variables such as TU_DEBUG, and the usual
way to set one on Android, the wrap.<package> property, is ignored on a user
build. It can be set and read back while never reaching the process, which makes
a flag that never applied look exactly like a flag that made no difference. That
is how the first attempt at this measured stock Turnip twice and called it a
result.

Read NAME=VALUE lines from <root>/driver_env.txt during initialize, before any
Vulkan instance exists, since Mesa caches each option the first time it is read.
A missing file does nothing, which is the normal case.
2026-08-10 03:45:22 -04:00
jpolo1224 32c6bc1de2 Write the resolution scale from the setting that has a control
Picking a scale and launching a game still rendered at native. The previous
attempt read the launch-time write from ps3.resolutionScale, which turns out to
be the wrong end of it: that field has no writer anywhere in the UI, so it holds
its default of 100 permanently.

applyTo pushed that default onto Video@@Resolution Scale, the same node the
upscale multiplier writes, and applyTo runs after the launch path, so the orphan
won every time. Changing the scale in game appeared to work only because nothing
calls applyTo again afterwards.

Emit the node from upscaleFloat instead, which is what the preset grid, the
custom percentage slider and the in-game overlay all write, using the same
conversion and clamp as the other writer so the two cannot disagree. Restores
the launch-time call to the multiplier it always used.
2026-08-10 03:31:18 -04:00
jpolo1224 9c62fbd3a8 Stop the PS2 upscale multiplier overwriting the PS3 resolution scale at boot
Picking a resolution scale and then launching a game ran at native. The UI kept
showing the chosen value, the config held the default, and changing it in game
worked, which made it look like the setting was not saving.

Both settings write the same native node. applyTo writes the PS3 percentage to
Video@@Resolution Scale, and renderUpscalemultiplier writes the ARMSX2-lineage
multiplier times a hundred to the same place, from the launch path, after
applyTo. So the last writer won and it was the one carrying a default of 1.0.
Changing the value in game appeared to work only because nothing writes the node
again afterwards.

Drive the launch-time write from the PS3 setting so the two agree. Same node,
one owner.
2026-08-10 03:27:52 -04:00
jpolo1224 cc63d66148 Count the barriers landing inside each render pass
Pass six spends 9.8 ms on 44 draws and 33 thousand vertices at ordinary
resolution, which is 300 ns a vertex. That is not vertex work, and the 2048
square shadow map next to it costs under a quarter of a millisecond with twice
the geometry, so it is not target size either. What is left is the GPU being
serialised inside the pass.

texture_barrier keeps the pass open on Android and issues a by-region
self dependency instead, which was the right trade against a tile store and
reload. But that barrier still makes a tiler resolve the tile and fetch it back,
and one per draw would cost about what pass six is costing. Nothing counts them.

Count barriers issued while a pass is open, per pass, and how many came from a
cyclic reference. If pass six shows one per draw the mechanism is named; if it
shows none, the serialisation is somewhere else and this rules out the obvious
candidate cheaply.
2026-08-10 03:16:01 -04:00
Megamouse f945ab62c0 Fix steam shortcut creation
Look for AutoLogin by first.
Look for MostRecent and Timestamp as fallbacks.
2026-08-10 09:10:42 +02:00
jpolo1224 f115c7b554 Say what the expensive passes are, not just which
Two passes hold 69% of GPU time and one of them, pass six, costs 76us a draw
against 2.4us in pass eight while holding 7% of the frame's draws. Rendering at
quarter resolution changed nothing, so it is not fragment work, and the ordinal
on its own says nothing about what the pass is for.

Record the render target size and the vertex count per pass alongside the draw
count. Size names the pass in the game's terms, since a shadow map, a reflection
and the main scene do not share dimensions. Vertices per draw separates a lot of
geometry from a lot of cost per vertex, which is the question the timing cannot
answer and which decides what a fix would even look like.
2026-08-10 03:09:49 -04:00
jpolo1224 5a6f32f93d Break the GPU draw total down by render pass
Rendering at a quarter resolution changed the GPU time not at all, which rules
out fill rate, fragment shading and tile traffic in one measurement, since all
three scale with pixels. What is left inside the passes is geometry, binning and
per-draw cost. It also retires the tile bandwidth theory the previous two
attempts were built on: that traffic would have fallen sixteen fold.

So the draw total needs splitting, and the timer already measures each pass
individually and only reports the sum. Report the distribution instead, keyed by
the pass ordinal within the frame: the frame structure is stable, so pass N is
the same logical pass each time, which is what makes it something to act on.

Count draws per pass alongside it, on the same ordinal. A pass that is expensive
holding few draws is expensive per draw; one holding most of the frame's draws
is carrying the geometry. Same milliseconds, opposite fixes.

Reporting only. No new timestamps and nothing recorded that was not already
being measured.
2026-08-10 03:04:10 -04:00
jpolo1224 6e15b16941 Revert "Clear at pass begin instead of reading the framebuffer to overwrite it"
This reverts commit 42e3d3b261.
2026-08-10 02:55:27 -04:00
jpolo1224 42e3d3b261 Clear at pass begin instead of reading the framebuffer to overwrite it
vkCmdClearAttachments needs the pass open, and the pass opens with LOAD_OP_LOAD,
so clearing a target reads the whole framebuffer into tile memory and then
throws it away. LOAD_OP_CLEAR skips the read. On a tiler that read is the whole
attachment every time, and this title runs about thirty passes a frame at 720p
with colour and depth.

Taken only when the clear covers the entire render area and no pass is already
open. A partial clear is not a load op, and ending an open pass to change its
load ops would store the framebuffer in order to discard it, which costs more
than it saves. Colour is all attachments or none, since a load op applies to the
attachment as a whole. Depth and stencil get separate bits because clearing one
and keeping the other is common.

Whether an open instance can serve a request now compares the key with the clear
bits masked off rather than the pass pointer. Load ops do not affect render pass
compatibility, so the two variants are interchangeable for an open instance and
for the pipelines inside it; comparing pointers would have ended the instance to
begin an equivalent one, paying the store and reload this is meant to avoid and
discarding the clear on the way. Callers that pass no key keep the old pointer
comparison.
2026-08-10 02:48:49 -04:00
jpolo1224 0cafae85c3 Open the GPU frame region on the path every frame takes
The collector had gathered eight frames in five thousand flips and its ring was
parked on slot zero with every slot unreset and empty. All of that follows from
one thing: the frame region was opened at device init and in flush_command_queue
only, and this title takes that path roughly never, so the region opened once at
boot, closed on the first submit and was never opened again.

Everything else depends on it. The slot's query range is reset when the frame
region opens, the ring only advances past a slot once something in it has
completed, and collection refuses a slot that was never reset. So a timer that
initialised cleanly and logged its tick period produced no report for an entire
session, which reads the same as a GPU with nothing to do.

Open it where the primary command buffer is actually begun for the next frame.
2026-08-10 02:36:22 -04:00
jpolo1224 1ad04fb13f Make the GPU collector say why it has nothing
The GPU timer initialises, reports its tick period, and then never produces a
report: eighteen RSX profiles came and went in one session against zero GPU
profiles. Collection has several preconditions and the report only prints once
three hundred frames have been gathered, so a collector stuck on any of them
prints nothing at all, which reads exactly like a GPU that is idle.

Log the collector's state periodically while it has nothing, with the slot
flags, the open regions and the drop count, so the precondition that is not
being met can be read instead of guessed at.

Also stop recording anything but the frame region into a slot that still needs
its reset. Writing a timestamp into a range that has not been reset is invalid,
and the reset only happens when the frame region opens, so a render pass that
begins first -- the ones flip() runs after next_frame has already rotated the
slot -- was writing into stale queries.
2026-08-10 02:32:18 -04:00
jpolo1224 9bad25466a Record the GPU draw region that was declared and never written
The GPU timer measures the whole frame, readbacks, blits and uploads, and the
one region it names but never records is draw. So the split it exists to provide
has been missing exactly where it matters: with the RSX thread no longer waiting
on a fence, the Adreno sits at 99% busy at its top clock and nothing says how
much of that is drawing the game.

Bracket the render pass at the only place one actually starts, not at the
wrapper, which early-outs when the same pass and framebuffer are already bound.
Roughly thirty passes a frame, comfortably inside the per-frame event cap.

Both timestamps sit outside the pass rather than inside it. On a tiler the load
at the start and the store at the end are the expensive part, and timing from
within would exclude the cost worth knowing about.

Take the command buffer by const reference, which is what the render pass
helpers hold and what the conversion operator already permits.
2026-08-10 02:26:45 -04:00
jpolo1224 d18f1fe55d Attribute render pass teardowns to the code that wanted them
Ending the open pass to change an image layout costs a tile store and a reload
on a tiler, and it happens about twenty times a frame out of twenty nine passes.
The counter on it says how many and never which: change_image_layout is reached
from seventy five call sites, tagging them by hand would be tedious and would
still miss the next one added.

Record the return address instead. Two levels, because image::change_layout
funnels most callers and one level would name that function for nearly
everything. Only recorded when a pass is actually open, so the count is
teardowns caused rather than layout changes attempted, and only while profiling
is armed, on a path taken twenty times a frame.

Reported as a symbol where the dynamic table has one and as a module offset
otherwise, which llvm-symbolizer resolves against the unstripped core.
2026-08-10 02:19:47 -04:00
jpolo1224 85c18bd67d Name the three biggest things inside FIFO decode
FIFO decode is 56.7% of the RSX thread now that it is no longer waiting on a
fence, and it is the enclosing scope of the dispatch loop, so it holds every
method handler body as well as the loop itself. 201 ns a dispatch is far too
much for reading a word and calling a handler, so the cost is in a handler or in
the per-dispatch machinery, and nothing in the report separates those.

Scope the two batching handlers and the FIFO cache refill. All three run a few
thousand times a frame at most rather than per dispatch, so unlike the earlier
attempt at a per-command scope none of them measures mostly itself.

Count calls, not methods. Both handlers consume a run and skip the rest, so
their share of the method histogram counts what they swallowed rather than how
often they ran, and dividing by it would price a batch as a single method.
2026-08-10 02:09:55 -04:00
jpolo1224 c78e48bd28 Wait for the GPU after recording a frame instead of before it
Neither vkGetFenceStatus nor vkWaitForFences with a zero timeout returns without
waiting on this driver: both measured 17-28 ms a call and neither returned
not-ready once in 300 frames. So the poll cannot be made honest at the call
site, and the previous commit's zero timeout changed nothing.

What can move is where the wait happens. Draining the present queue at the first
draw of a frame meant the CPU started recording only once the GPU had finished,
and frame time became GPU plus CPU rather than the larger of the two: 42 ms made
of 27.7 GPU and 14.3 CPU, which is the two of them end to end. Stop draining
there and let the throttle at flip bound the pipeline, which is the same wait
placed after the frame's recording rather than in front of it, so recording runs
while the GPU is still busy.

The rotated context should already be retired by then. If it is not, borrow the
aux context as before, and if that is busy too, wait for this one specifically
rather than trip the ensure behind it.
2026-08-10 01:59:49 -04:00
jpolo1224 f592ebb752 Stop asking the driver a question that answers by waiting
vkGetFenceStatus measured 19.7 ms per call on Adreno and returned VK_NOT_READY
zero times in 300 frames. Every caller of poke() wants "is it done, do not
wait", so the one call per frame turned an intended poll into a full GPU sync
and ran the CPU and the GPU in series: frame time was CPU plus GPU rather than
the larger of the two, with the GPU only 66% busy at less than its top clock.
Use vkWaitForFences with a zero timeout, which is specified to answer without
waiting.

That poll was also the only thing bounding the pipeline, because a queue whose
oldest entry is always retired before the next is added never holds more than
one. With an honest answer the frames accumulate, so bound them on purpose, one
below the frame context count: the queue and the context rotation advance
together, so retiring the front is what frees the context about to be handed
out. Allowing the full count would route every frame through the single aux
context borrow and hit the ensure behind it.

The remaining wait is real frame pacing and is charged to swap_wait, where it
can be read.
2026-08-10 01:53:02 -04:00
Zulux91 27465da4ce Improve ARM64 CPU detection and Android device diagnostics
The fallback CPU table was missing cores found in recent handheld SoCs
(Cortex-A510, A715, X3, A520, A720, X4). Because get_cpu_name() bails out
when any detected MIDR is unknown, a single missing core sent the whole
lookup to the cortex-a78 fallback whenever LLVM host detection returned
"generic".

Display names were also reused as LLVM -mcpu values, which happens to work
for the Cortex names but not for Qualcomm Oryon: the display name lowercased
to "x-elite", which is not an LLVM processor, so the JIT silently lost
per-CPU scheduling. Entries now carry an explicit canonical LLVM name
alongside the human-readable one; get_cpu_brand() keeps using the latter.
The Qualcomm entry is named "Oryon" rather than "X-Elite" because MIDR
0x51/0x001 only identifies an Oryon core, not the SoC it sits in.

MIDRs cannot identify the SoC at all, which made bug reports ambiguous.
Android's own SOC_MANUFACTURER/SOC_MODEL are now passed to the core and
logged as a separate "SoC:" line, so SoC identity, core topology and the
resolved LLVM target are three distinct values. The LLVM target reported by
system info now comes from the same resolution path the JIT uses, rather
than from the fallback alone, so it no longer disagrees with the target
actually compiled for.

The Vulkan renderer logs one verdict for the adapter it selected, recording
whether BC1-BC3 support keeps DXT textures compressed or whether they are
decoded on the CPU. It sits in render_device::create rather than where the
flag is resolved, because physical_device::create runs for every GPU of
every instance, and not in TextureUtils, whose fallback branches run per
texture and per mip level.

SoC information travels through a new optional _rpcsx_setSocInfo export
instead of an added _rpcsx_initialize parameter. The core is dlopen()ed and
can be updated independently of the JNI glue, so changing an existing
export's signature would make older glue call it with a garbage argument.
Older glue simply never calls the setter, and newer glue null-checks the
symbol against older cores.

No JIT feature policy and no texture decoding behaviour changed.

Verified on an AYN Odin 3 (ayn CQ8725S, 8x Oryon, Adreno 830) running
Turnip 26.2.99: SoC line reads "ayn CQ8725S (Snapdragon 8 Elite-class)",
the brand line reports Oryon rather than X-Elite, the JIT resolves to
oryon-1, and a single BC verdict reports the GPU path. That BC result
applies to the Turnip driver tested; stock-driver behaviour is unmeasured.
2026-08-10 01:43:45 -04:00
jpolo1224 76b556a492 Count the fence polls before believing what they cost
Resource destruction was the stated suspect and measured 0.056 ms, so the time
is in vkGetFenceStatus, which is over half the RSX thread. Every call site that
reaches it appears to run once or twice a frame, and a status query that blocks
for milliseconds would be a driver problem while one called a hundred thousand
times would be ours. Nothing in the report distinguishes those.

Count the calls and how many come back not ready. This is the same denominator
the FIFO buckets needed twice already, once for packets against commands and
once for draws against setup.
2026-08-10 01:38:34 -04:00
jpolo1224 93585560c2 Separate retiring GPU objects from noticing the fence
The present check kept its 18.9 ms after the fence wait and the reclaim both
measured zero, which leaves the poke, and the only thing in a poke that can
sleep is the event completion callback. Without multithreaded RSX that callback
runs inline, and popping an event scope runs the destructor for every GPU object
that event retired, so the RSX thread frees a frame's worth of images and memory
through the kernel driver before it can record the next draw.

Scope the destruction and the fence status query separately. If neither holds
the time, what is left is the lock at the top of the poke, and that is a
different bug again.
2026-08-10 01:33:13 -04:00
jpolo1224 5171e21dc3 Split the present check into waiting and working
The mid-draw present check turned out to be two thirds of the RSX thread, which
the previous commit could only say as one number. It covers three things that
mean opposite things: a fence wait, a poke that takes a shared lock, and the
per-frame resource reclaim. A wait says the GPU is the bottleneck and every CPU
change aimed at this path was aimed at nothing; the reclaim says the opposite.

Scope the fence wait and the reclaim separately, so whatever is left in the
present check bucket is the poke and its lock. Count entries and cleanups too:
the block is written as a rare async flip fixup, so how often it runs is the
first thing worth knowing about it.
2026-08-10 01:24:04 -04:00
jpolo1224 dc884d6599 Give the draw setup remainder a name instead of a plausible label
Draw setup was 66.7% of the RSX thread, but the bucket only ever held whatever
VKGSRender::begin and end did not charge to a nested scope. Everything with a
body of its own now carries one: the surface write barriers, the render target
on_write pass, the temporary texture release, the mid-draw present check, and
rsx::thread's own prologue and epilogue, which were unscoped on both backends.

Count draws too. A large per-draw bucket is a lot of draws at a fair price or a
few at an unfair one, and those want opposite fixes; the FIFO buckets already
learned that lesson the hard way when a per-packet figure was read per command.

Draw setup keeps the leftovers, which is now the draw clause loop and nothing
that can hide 23 ms.
2026-08-10 01:14:00 -04:00
jpolo1224 e13fc184f0 Make the redundant vertex program check actually compare
The two-point probe read the incoming words as be_t<u64>, an eight byte swap,
and compared that against a destination written by copy_data_swap_u32, which
swaps each word on its own. The wide swap also exchanges the two words, so the
comparison was (w0,w1) against (w1,w0) and could only match when w0 equalled w1.
It never reported a match.

Every upload therefore set vertex_program_ucode_dirty. That forces a full vertex
program re-analysis per draw clause, drops the program cache hint, nulls the
bound program so load_program runs again, and re-uploads the transform constants
unconditionally. Sonic '06 issues 8088 of these a frame against 3429 draws, and
a corrected profile puts 24.7ms of a 36.4ms frame in draw setup, which is what
all of that lands in.

Rotating the source back by 32 bits puts both sides in the same word order. The
change can only remove spurious invalidations: a clean verdict from the probe is
still confirmed word for word by the full compare below it, so a false clean is
not reachable.

Upstream inherited, introduced in ae39c5b8cb.
2026-08-10 01:01:48 -04:00
jpolo1224 42d33d7fd7 Attribute the per-draw work instead of billing it to FIFO decode
fifo_decode is the enclosing scope of the whole RSX loop, and the profiler is
exclusive, so it holds whatever no nested scope claimed. VKGSRender::begin()
carries a scope and end() did not, so essentially every per-draw cost landed
there: load_texture_env's texture cache search and sampler lookup, the vertex
and fragment ucode analysis, and the write barriers. shader_translate and
barrier had no instrumentation sites anywhere in the tree.

That is why the bucket read as 100% of a 29ms frame while the decode loop itself
only accounts for a couple of milliseconds: 36728 packets and 48737 dispatches
cannot cost 29ms when the loop body is an inlined exchange, a table load and an
indirect call.

Scopes added to end(), load_texture_env and analyse_current_rsx_pipeline, so the
next capture shows where the frame actually goes.
2026-08-10 00:57:22 -04:00
jpolo1224 5636c9f3ff Stop paying per-packet and per-argument costs in the FIFO loop
Three changes to the same loop, all measured against 36728 packets and 48737
dispatches per frame in Sonic '06.

GET is published on a bounded lag rather than every packet. It is a release
store into guest DMA memory, and get shares a 64-byte line with put which the
guest PPU writes from another CPU cluster, so each publish was a cross-cluster
coherence miss. The guest reads GET to size its free ring space and is far ahead
of us here, FIFO stalls measuring 0.1 a frame, so lag is invisible to it.
Anything that can idle or block publishes immediately: the put wait in inc_get,
the NOP path, and set_get.

The FIFO accuracy setting is snapshotted once per packet instead of being read
per argument. Reading it goes through a seq_cst atomic load, which on ARM64 is
an ldar the compiler cannot hoist out of the loop.

The again poll is relaxed. That flag is only ever set by this thread, by the
handler invoked immediately before, so sequential consistency buys nothing and
cost another ldar per dispatch.
2026-08-10 00:43:34 -04:00
jpolo1224 1c371cfad7 Count FIFO dispatches, not packets, and stop paying for two hot loads
Three separate fixes to the same hot path.

The profiler's FIFO figure counted the wrong thing. g_fifo_commands is
incremented once per run_FIFO entry, and one of those drains a whole packet, so
dividing by it priced a packet rather than a method. Sonic '06 averages about 17
methods per packet, so the reported 612 ns per command was really 612 ns per
packet and the per-method cost was closer to 36 ns. Cross-checks: 413.6 FIFO
refills a frame at 4096 bytes is 1.69 MB, about 423000 words, which 24248
packets can only consume at roughly 17 words each. Dispatches are now counted
where they are dispatched, both figures are reported, and the per-method
histogram divides by the right one. The line is labelled packets, and notes that
fifo_decode is a catch-all holding every handler body too, since no handler
carries its own scope.

rsx_state::decode was a cross-TU call per dispatched method. The body is one
exchange, but the definition lived in rsx_methods.cpp and LTO is disabled
project-wide, so it never inlined, and being opaque it also forced the caller to
reload its context pointer afterwards. Moved to the header.

set_transform_constant and set_transform_program read ctrl->put through a
seq_cst load. That is an ldar on ARM64, on a cache line shared with ctrl->get
which the guest PPU writes from another cluster, in the two hottest handlers in
this title. Relaxed: a stale value only shrinks the batch, and the remainder is
picked up on the next call.
2026-08-10 00:31:26 -04:00
jpolo1224 b971d81862 Byte-swap four words at a time on ARM64
copy_data_swap_u32 and its compare variant are assembled by asmjit under
ARCH_X64 only. Every ARM64 build fell through to the scalar per-word loop,
reached through a function pointer so it could not be inlined, and LTO is
disabled project-wide so nothing recovered it afterwards.

It is not a cold path: transform constants, transform programs and vertex data
all upload through it, and a Sonic '06 profile on a Snapdragon 8 Gen 2 put 82.5%
of the RSX thread in FIFO decode with thousands of these blocks per frame.

vrev32q_u8 reverses bytes within each 32-bit lane, which is the same swap the
scalar path does per element. The compare variant accumulates differences and
reduces once at the end rather than branching per element. Checked against the
scalar version over 20000 randomised trials at counts 0 to 39, covering every
tail remainder, for both variants: identical output and identical return value.
2026-08-10 00:28:19 -04:00
jpolo1224 6581973646 Restore the constant load pointer, not the program load pointer
transform_constant_load_modifier_barrier decoded its argument into
NV4097_SET_TRANSFORM_PROGRAM_LOAD. The barrier is pushed by
nv4097::set_transform_constant_load, so it should target
NV4097_SET_TRANSFORM_CONSTANT_LOAD.

A title that moves its constant load pointer mid-draw therefore had the move
dropped, leaving every constant after it written at the old offset, and had its
vertex program upload position overwritten with a constant index at the same
time.
2026-08-10 00:28:19 -04:00
jpolo1224 cdcf384df2 Block on the GPU fence instead of spinning on it
An unbounded wait polled vkGetFenceStatus in a tight loop with nothing but a
pause hint between calls. command_buffer::flush() takes that path for the submit
fence, so it is what a frame does while it waits on the GPU: a core pinned at
100% for the whole wait, hammering a driver entry point while the driver is
trying to do the work being waited on.

Cheap on a desktop with cores to spare. Not here, where it competes with the SPU
and PPU threads for a handful of cores. Arkham City measured 24ms of a 53ms
frame in this function with the GPU only 71-77% busy, which is what a stall
looks like when the waiter is too busy spinning to prepare the next submission.

Polls briefly first, since most waits are for a fence about to signal and
blocking would cost a syscall and a wake-up for nothing, then hands the wait to
vkWaitForFences so the driver can sleep the thread. The blocking call was
already there, three lines up, used only when a finite timeout was supplied.
Same shape as wait_for_event below, which already had this treatment.
2026-08-10 00:04:14 -04:00
jpolo1224 b55cd3dd44 Show installed licences
Installing a licence is a copy into exdata, a directory nothing on the screen
read, so a success looked exactly like a failure: a licence belongs to no title,
never appears under Installed titles, and left nothing visible anywhere in the
app. Reported as the .rap doing nothing, when the file had in fact been written
correctly.

Listed now beside the installed titles, refreshed on the same events.
2026-08-09 23:49:40 -04:00
jpolo1224 58bf1b3b13 List only real content under Installed titles
Every directory under dev_hdd0/game was listed with an Uninstall button beside
it. RPCS3 keeps its own lock directory in there, get_hdd0_locks_dir() being
get_hdd0_game_dir() + "$locks/", so the screen offered to delete the emulator's
lock state, and any folder a failed install left behind was offered as a title.

A PARAM.SFO is the test now. Game data installs keep theirs and stay listed on
purpose: a 1.1GB BLUS30464_INSTALL is the kind of thing someone opens this
screen to reclaim, bootable or not.
2026-08-09 23:34:49 -04:00
jpolo1224 0bd30e3c9b Log directory entries at trace, not warning
sys_fs_readdir fires once per directory ENTRY, unlike opendir and closedir
either side of it which fire once per operation. A game scanning its own USRDIR
emits a line per file: one such scan measured 346 lines in 22ms, during boot,
for no diagnostic gain.

Moved to trace, which is where the other per-datum calls already sit
(sys_fs_read, sys_fs_write). opendir and closedir stay at warning, so a scan is
still visible in the log without being enumerated.
2026-08-09 23:00:04 -04:00
jpolo1224 79df76242b Request only the Vulkan version the loader supports
The instance asked for 1.2 unconditionally. A loader that predates it may answer
VK_ERROR_INCOMPATIBLE_DRIVER to a higher request, and the spec tells applications
to check the version first for that reason, so on those devices the renderer
never started at all.

Queried through the global procedure address, since vkEnumerateInstanceVersion is
itself a 1.1 entry point and its absence means 1.0, then clamped. A no-op wherever
1.2 or better is available, and logged when it is not so a device report says so.
2026-08-09 22:38:10 -04:00
jpolo1224 f73143fad9 Release the JNI references the progress reporter takes
Every JNI object handed to native code is a local reference, reclaimed only when
the frame that created it returns to Java. The frames this runs on do not return:
the main thread processor and the compilation queue are infinite loops inside a
single JNI call.

Progress took one reference per instance from FindClass and one per report() from
NewStringUTF, and released neither. There is no DeleteLocalRef, PushLocalFrame or
NewGlobalRef anywhere in the native tree. The progress dialog server pushes
several updates per tick and a firmware precompile emits thousands of ticks, so
ART's local reference table filled and the runtime aborted.

Time-proportional, which is why it showed as a crash during firmware install on
slower devices and not on faster ones.

Copy construction is deleted along with it: the class owns a reference now, and a
copy would have had its destructor release one the original still used.
2026-08-09 22:38:10 -04:00
jpolo1224 774636642f Stop a per-title workaround following the user into every game
The Android port keeps one global config.yml: settingsSet persists through
SaveSettings(g_cfg.to_string(), "") and an empty title id is the global path.
apply() returned early for any title without an entry, writing nothing, so
Uncharted 3's Stub PPU Traps = 1 stayed set once it had been booted and every
game launched afterwards ran with a PPU that silently skips an instruction on
any trap rather than stopping.

Nothing said so on screen and nothing else writes that node: it is not in the
curated push, and CoreSettingOverrides only replays paths the user recorded
themselves.

Every managed path is now written on every boot, this title's value where it has
one and the upstream stock value where it does not. Anything added to BY_SERIAL
has to gain its default in STOCK.
2026-08-09 22:31:27 -04:00
jpolo1224 2d40dd1627 Scan dev_hdd0/game as installed titles, not as a folder to search
The recursive scan descends into anything that is not itself a game folder and
then accepts any file whose extension is in gameExtensions. "img" is one of
them and a title's own data is full of them, so once a package unpacked over
dev_hdd0/game the library filled with GTA IV's archives: manhat01, props_ab,
vehicles, script, weapons.

dev_hdd0/game is the emulator's own install root and holds one directory per
title, so it is now read that way. A direct child is a title or it is not
listed. Folders a user pointed us at keep the recursive scan, because games
legitimately sit at any depth there.

The extractor no longer unpacks into this directory either, but the library
should not have depended on that, and existing installs still have the debris.
2026-08-09 22:29:18 -04:00
jpolo1224 cd96d55d05 Refuse to unpack a package into the games root
A package's install directory is taken straight from its own metadata and was
never checked. Both sources can produce nothing: read_metadata sizes the string
to 9 and reads the title ID over it without testing the result, so a short read
leaves nine NUL bytes, and the DLC path takes c_str() + 8, which is empty
whenever byte 8 is a NUL.

Appending either left the destination as dev_hdd0/game itself, so the package
unpacked its contents over the games root. Users reported a library full of
asset directories, storage consumed with nothing listed as installed, and
folders that outlived uninstalling the title, because uninstall only removes
dev_hdd0/game/<TITLEID>. It looked random because it depends on the individual
package's metadata.

Checked against c_str() so the nine-NUL case reads as empty. Separators and dot
entries are refused as well: this is one path component chosen by the package
and it has no business pointing anywhere else.
2026-08-09 22:29:18 -04:00
Zulux91 c2b5f0c400 Add missing ARM64 instruction-cache maintenance to the JIT
While chasing an unrelated SPURS hang I noticed the JIT publishes
freshly written code on ARM64 with no instruction-cache maintenance at
all. A grep for clear_cache or flushInstructionCache over the JIT layer
comes back empty. The branch-rewrite sites only issue ISB; DSB ISH,
which performs no D-cache clean or I-cache invalidation and is ordered
backwards for self-modifying code besides. On ARMv8 a correct
publication needs the DC CVAU / IC IVAU broadcast sequence; x86 has a
coherent instruction cache, so none of this was ever visible there.
All sites use the bundled asmjit::VirtMem::flushInstructionCache(),
which emits that sequence portably across toolchains.

This covers every publication path I could find:

- MemoryManager1::finalizeMemory() and MemoryManager2::finalizeMemory()
  were both no-ops. RuntimeDyld calls finalizeMemory() after writing
  code and relies on it for cache maintenance, so LLVM emitted PPU and
  SPU code was never flushed. MemoryManager1 serves the primary PPU
  JIT, MemoryManager2 the SPU JIT and auxiliary engines. Both managers
  now record code section allocations and flush them on finalize. I
  confirmed at runtime that the MemoryManager2 path executes (about
  12800 calls per cold boot).
- jit_runtime_base::_add() copies asmjit output into executable memory
  with no flush.
- jit_runtime::finalize() restores an executable code snapshot in place
  during emulator restart with only the ISB/DSB pair.
- spu_runtime::rebuild_ubertrampoline() publishes a hand-written
  trampoline via CAS with no flush; the flush now happens before the
  publication.
- spu_runtime::make_branch_patchpoint() writes a patchpoint byte by
  byte and returns it with only the ISB/DSB pair.
- Both 16-byte branch-site rewrites (dispatch and branch) atomically
  overwrite live code and only issued the ISB/DSB pair.

The ISB/DSB pairs adjacent to the new flushes are removed along with
their misleading "flush all cache lines" comments: the flush helper
already issues the trailing barriers, and the pairs never performed
any cache maintenance in the first place.

I want to be upfront that this was not the cause of the hang I was
debugging (a same-item compilation race, fixed separately), and I have
not observed a failure that this change alone fixes. It is a latent
correctness issue on any ARM64 host: nothing prevents another core
from fetching stale instruction bytes for freshly published code.
2026-08-09 22:19:32 -04:00
Zulux91 1847433eb5 Serialize LLVM compilation of identical SPU programs
Five SPURS kernel threads executing the same uncached code at the same
address all reached spu_llvm_recompiler::compile() for one spu_item.
add_empty() returns the existing item for an identical program without
telling the caller it did not insert, and the entry-point dedup only
catches equivalent code at a different address. Each thread then
compiled the program with its own LLVM instance, racing the compiled
pointer publication, the ubertrampoline rebuild, and the waiter
notification.

On my 8-core ARM64 device this wedged SPURS bring-up on every single
cold-cache boot of Virtua Tennis 4 (BLUS30529). The kernels ended up
parked polling zeroed workload state, the PPU main thread blocked
forever in sys_event_queue_receive on a queue no SPU would ever signal,
and the title never reached the menu. Warm boots never hit it because
cached programs are compiled before SPU execution starts, one
presentation per program. When I instrumented the compile path I saw up
to five concurrent compilations of a single item, around 790 collision
events per boot, with duplicates accounting for roughly two thirds of
all cold compilation work.

This change gives spu_item an explicit LLVM compilation state
(unclaimed, compiling, complete, failed). The first compiler claims the
item; later arrivals wait and take the published result, mirroring the
existing dedup-wait path. I made the claim a state on the item rather
than an inserted-flag from add_empty() so that an item pre-inserted by
spu_fast is still claimed by the first LLVM worker, which preserves the
asynchronous optimized replacement on x86-64. A scope guard marks the
item failed on any early exit so waiters cannot be stranded.

The pre-existing wait for relocated duplicates (same program, different
entry point) is also covered: it still waits on the compiled pointer,
because that result can be published by spu_fast from the asmjit path
which never touches the LLVM state, but it now observes the failure
state on each wakeup with a bounded timeout, and the failure guard
wakes those waiters too. Without this an owner that bailed out early
would have stranded them forever.

I verified this on device: 11 out of 11 cold boots stalled before the
change, 7 out of 7 pass after it, plus 2 out of 2 warm controls, and
Mirror's Edge now reaches gameplay past its previous SPURS stall.
Cold-boot SPU compilation dropped from about 12900 blocks to 3900.
2026-08-09 22:19:32 -04:00
jpolo1224 a862c4b8b5 Install licence files by name, the way upstream does
Every .rap failed. A .rap is 16 raw bytes of key and carries nothing that says
which content it unlocks; that lives in the filename, as the content id. Upstream
copies the file into dev_hdd0/home/<usr>/exdata/ under its own name and is done,
so the name is the whole mechanism.

This called the native installKey with an empty game path instead. That path
decrypts the GAME's EBOOT to read an NPDRM header out of it, so with no game path
there was no EBOOT, no header, and the install could never succeed. Handing it a
bare file descriptor had already thrown the name away regardless.

Licences are now handled before any descriptor is opened. Reported for Resident
Evil 4 HD and for DLC licences generally.
2026-08-09 22:14:40 -04:00
Megamouse ec208289fa elf: remove unnecessary -1 on both sides of a size check to prevent underflow 2026-08-10 02:34:41 +02:00
Megamouse b93c4733a8 Fix OOB buffer read in TRPLoader::LoadHeader 2026-08-10 01:33:53 +02:00
jpolo1224 ce5bd4687c Release 0.3.1 2026-08-09 02:57:13 -04:00
jpolo1224 3f91dfac12 Bundle the Sonic '06 graphics fix and enable it
SONIC THE HEDGEHOG (2006) renders only its HUD and skybox and flickers
everything else in and out of existence. That is upstream RPCS3 issue
#4122, open since 2018: a PPU/SPU race where an SNR is overwritten while
still non-empty, so the SPU jobs feeding geometry lose their signal.

Upstream never fixed it in code. The fix is elad335's canary patch, which
hooks RPCS3_HLE_LIBRARY:WaitForSPUsToEmptySNRs -- a function that already
exists in the core and has no other user. The patch is not in the official
feed: neither our stored database nor a fresh pull from rpcs3.net carries
a single BLUS30008 entry, jumpf op, or RPCS3_HLE_LIBRARY reference. Desktop
users add it by hand through the Patch Manager's import button, which has
no equivalent here, so on Android the game was simply broken.

Ship it as an asset and merge it into patch.yml at boot, before the core
reads that file and once the config directory is known to exist. Enable it
rather than only listing it: a user who has to find and tick a box before
the game renders has already decided the emulator is broken. Gated on a
stored revision, not run every boot, so turning it off sticks.

Everything ARM64 needs is already in this tree -- the calloc code-cave
registry, the is_faux_function guard that keeps the tail-call return trap
from firing on a patch-point, and the PPU LLVM filter for patched
functions -- so the patch applies here rather than crashing as it once did
on ARM.

Keyed by PPU hash, so it covers only the dump it was built for; other
regions and revisions need their own entry.
2026-08-09 02:57:07 -04:00
jpolo1224 aa7a37be75 Report real CPU usage on Android
get_per_core_usage() fills the per-core vector with zeros and then wraps
the whole Linux /proc/stat body in #ifndef ANDROID, because an app cannot
read the per-cpu lines. The exclusion left the zeros in place, so the
monitor did not go quiet -- it reported an idle machine. On device it
logged "CPU Usage: Total: 0.0%, Cores: 0.0%, 0.0%, ..." while three
emulator threads were pegged at 100%, which hides exactly the class of
problem the monitor exists to surface.

Report process usage from times(), which is POSIX and readable by our own
process, and leave the per-core vector empty so perf_monitor prints no
"Cores:" list rather than a fabricated one.
2026-08-09 02:56:50 -04:00
jpolo1224 d36e0b93a9 Stop the RSX offload thread spinning when idle
When the transfer queue was drained the offload thread called
std::this_thread::yield() in a tight loop, which is a sched_yield()
spin rather than a wait. Measured on an Adreno 740 handheld it burned
1142s of CPU over a 1145s session, 85% of it system time, for no work
at all -- a whole core taken from the RSX and SPU threads that need it,
plus the battery and thermal budget that goes with it.

Wait on the queue instead. lf_queue::push notifies on the empty to
non-empty transition so a real job still wakes the thread immediately
and transfer latency is unchanged, and if a push lands between the
emptiness check and the wait the atomic is already non-zero so there is
no lost wakeup. The timeout is only there so the loop can re-check
thread_ctrl::state(): aborting pushes nothing, so an untimed wait would
never return and shutdown would hang instead of spin.

dma_manager::sync() is unaffected -- it only waits while work is
outstanding, which is exactly when the thread is not idle.
2026-08-09 02:56:42 -04:00
Walter 6d42df0ffc CPUTranslator: Add missing Intel Intrinsic header
MSVC compiles without it, but other compilers need it to be explicitly included.
2026-08-09 09:13:41 +03:00
Walter 502ea1f436 CPUTranslator: Additional constant folding for intrinsics
LLVM is unable to constant fold its X86 intrinsics directly, so this patch adds manual evaluation by calling their equivalent Intel Intrinsic.
2026-08-09 09:13:41 +03:00
jpolo1224 4a7ef6f4c8 Match RPCS3's default for PPU Vector NaN Handling
Ours was false against upstream's true, so the curated push wrote false over the
node on every boot and every install ran with an accuracy fixup disabled that
RPCS3 ships on. Games that depend on it get NaNs through vector maths, which
shows up as geometry behaving impossibly rather than as any kind of error.

The disagreement was not free either. The node feeds ppu_settings::fixup_vnan,
which is part of the compiled PPU object filename, so flipping it renames every
object and the next boot recompiles all of them. A device here carries both
variants, 76 modules under one key and 58 under the other, from one flip.

Audited the other two settings in that key: Accurate Cache Line Stores and Use
Accurate DFMA both already match upstream. This was the only one adrift.

All three now say that changing them rebuilds compiled PPU code, which is the
part that had no way of being known from the screen.
2026-08-09 01:37:24 -04:00
jpolo1224 9e70983a49 Release 0.3
versionCode 4, versionName 0.3.
2026-08-09 00:53:34 -04:00
jpolo1224 65898619c3 Name the threads that did not stop
The unresponsive-stop callback reported that a stop was hanging but not what was
holding it, and that is the one thing that cannot be recovered afterwards: from
outside, a wedged shutdown looks like a sleeping thread with no CPU time and no
reason attached.

Enumerate the SPU threads still registered and log their cpu_flag state. Whether
exit was ever delivered, whether the thread is parked in a wait, or whether it
never left its initial stopped state are three different faults with three
different fixes, and the flags separate them.

This is what exposed the failed savestate: the thread that would not stop was
debris from a save that had already been abandoned, not a shutdown bug.
2026-08-09 00:35:06 -04:00
jpolo1224 8dded5deaf Hold Compatible Savestate Mode at the upstream default
It was turned on so savestates could save at all, and savestates are no longer a
feature: a PS3 state runs 500MB to 3GB, which fills a phone in a handful of
saves. What is left is the cost, and it is a real one, which is why upstream
defaults it off.

Written as false rather than dropped from the push. It shipped as true for a
while, so installs from that window have it persisted in config.yml and would go
on paying SPU performance for something nothing uses.

Also drops the thumbnail read logging, which did its job: it proved the read
path was never called, because the in-game menu has its own numbered slot list
and only the touch overlay's picker asks for a preview.
2026-08-09 00:34:44 -04:00
jpolo1224 26e4a93c4e Turn on Compatible Savestate Mode
Savestates could not work without it. Saving locks every SPU thread into a state
it can be serialised from, and with this off that lock fails on any title with
SPU work running. The save is abandoned with "failed to lock SPU threads
execution", and the SPU it gave up on then never answers the stop request, so
the join thread spins and the app hangs on the next save or load.

The deadlock chased through the SPU shutdown path was this setting all along.
The state on disk looked valid because a previous attempt had written one.

Upstream defaults it off for SPU performance, which suits a desktop where
savestates are optional. Here they are on the in-game menu and the touch
overlay, and a save that wedges the emulator costs more than slightly slower
SPU emulation.
2026-08-09 00:16:58 -04:00
jpolo1224 7fe099f40c Report a stop that is not responding
RPCS3's stop watchdog calls on_emulation_stop_no_response once shutting the VM
down has taken about ten seconds. That callback was a no-op here, so the one
thing upstream does about a deadlocked stop, say so, did not happen: the join
thread went on spinning, the overlay sat at its last figure, and the app looked
frozen with nothing in the log to explain it.

Reported rather than aborted. The desktop build offers to terminate but asks
first, and slow is not the same as stuck: a savestate write was measured here
taking 71 seconds legitimately, so killing the process on a timer would trade a
hang for a corrupted save.
2026-08-09 00:11:28 -04:00
jpolo1224 bb4ec4fc13 Add save slot thumbnails
The core already renders the finished frame and hands it to take_screenshot
whenever a screenshot is asked for. That override was empty, so the picture was
built and thrown away and every slot tile had nothing to draw.

Cached rather than captured on demand: a save kills the VM, and the point where
the state is known to be on disk is after_kill_callback, by which time there is
no renderer left to ask. So the save requests a frame on its way in, the
override catches whatever the renderer produces, and the capture step writes it
out once the state has landed.

Stored downscaled to a ~320px edge as AX3T + width + height + RGBA, and
re-encoded to PNG on the Kotlin side, which is where a Bitmap can be built from
raw pixels directly. That keeps a compressor out of the core for something only
ever drawn as a tile. A slot with no picture still loads.
2026-08-09 00:04:33 -04:00
jpolo1224 68666237b3 Boot the savestate after teardown finishes, not during it
Loading a slot closed the game and dropped to the library. BootGame failed in
the same millisecond it was called, without ever reading the file, and the
Emulation Join Thread warnings then ran for seconds after: the previous VM was
still coming down while BootGame was already being asked to bring the next one
up, so it bailed.

boot_current_game_savestate gets away with that sequence because it runs from a
shortcut handler rather than from inside the emulation callback queue. Kill
first and boot from after_kill_callback, which is the shape the save path uses
and the reason that one works.

The boot result is logged too. A bare failure could not tell a corrupt state
from a version mismatch from a path the loader never accepted.
2026-08-08 23:54:32 -04:00
jpolo1224 e4d99dc83d Show which save slots are occupied
Every slot tile read as empty and Load was disabled on all ten, whatever was on
disk. Occupancy comes from getGamePathSlot, which was a stub returning the empty
string, so saving had been working with no way to see that it had.

Answer it from hasState, which was written for this and had no callers. The
subtitle is the title id rather than the state's path: the picker strips to the
last segment and drops the extension, which would render a real path as
slot0.SAVESTAT.
2026-08-08 23:49:39 -04:00
jpolo1224 fbda9ce4d3 Give librashader the present surface's real format
RetroArch shaders shifted the colours, pink over anything green. The output
image handed to librashader is the swapchain image but the format reported with
it was the source framebuffer's. librashader builds its output view and render
pass from that, so with a BGRA swapchain and an RGBA source it wrote through a
mismatched view and swapped red with blue instead of converting.

Only the shader path was affected because the bilinear and nearest passes reach
the swapchain through vkCmdBlitImage, which converts formats itself. Carry the
swapchain format through to the pass, refreshed per frame so a swapchain rebuilt
at a new format cannot leave a stale value behind.
2026-08-08 23:41:53 -04:00
jpolo1224 43d1add7ec Make the save slots addressable
RPCS3 keeps savestates as a rolling history addressed by age, 1 being the most
recent, and the app offers ten numbered slots. The slot was dropped on save and
read as an age on load, so saving to slot 3 pushed a new newest state and
loading slot 3 fetched the fourth-newest. States appeared to wander between
slots on their own.

The history stays RPCS3's. A slot is now a copy parked under
savestates/<title>/armsx3_slots/, in its own directory because get_savestate_file
derives the next auto id by listing the title's directory and must not be fed
names it never generated. Copy rather than move: the restart after a save boots
from the file the core just wrote.
2026-08-08 23:41:53 -04:00
jpolo1224 72b6ce0449 Default the D-pad to 7% spacing
At 0 the four direction keys meet in the middle, which only reads correctly on
the flat built-in drawables. The bundled skins draw four separate keys and they
came out as one clumped blob.

Migrated once for existing installs: the key is written by the bulk save, so
anyone who had ever opened a touch setting already had 0 stored and would never
have picked up a new default. Only a stored 0 is rewritten.
2026-08-08 23:41:53 -04:00
jpolo1224 23fe26fdb9 Keep the Back button on screen in All Core Settings
The button was always there. The LazyColumn above it had no weight, so in a
Column it took the whole remaining height and left nothing for the row below,
putting Back off the bottom of the screen. With a controller you could still
leave; on a touch-only device this was the one screen with no visible way out.

Weight the list so it shares the space instead of consuming it.
2026-08-08 23:41:53 -04:00
jpolo1224 aaf3322d4e Recover skin previews from a manifest with trailing commas
No preview rendered for any downloadable skin. The published manifest carries a
trailing comma before two of its closing braces, which org.json rejects, so
parseManifest returned null and fetch fell through to the git tree, which only
ever reported filenames and set previewPath to null for everything. One comma in
a file this app does not own cost the whole index.

Retry the parse with trailing commas dropped, string aware so a name containing
a comma survives, and pair Previews images to skins by name in the tree fallback
so a broken manifest costs detail rather than every thumbnail.
2026-08-08 22:39:37 -04:00
jpolo1224 b7eff5bb12 Migrate existing installs onto the bundled default skin
Changing DEFAULT_SKIN_ID moved nobody. resolveRaw only falls back to the default
when nothing was ever stored, and every install from before the change has a
stored value already, so existing users kept the old pad and reported the new
default as not applying.

Rewrite the stored global skin once, and only when it is absent or the explicit
no-skin sentinel. An install pointing at a real skin chose it on purpose. The
flag is set either way so picking the ARMSX2 row afterwards sticks.
2026-08-08 22:39:37 -04:00
jpolo1224 b4026baacf Name the ARMSX2 skin row and tag the active default
The second row read 'Built-in (default)', which was accurate when the built-in
drawables were the default and wrong the moment a bundled pack took over: it
still claimed to be the default while ARMSX3 Textured actually was. Name it
'ARMSX2' for what it is and tag whichever row matches DEFAULT_SKIN_ID, so the
label follows the constant instead of restating it.
2026-08-08 22:39:37 -04:00
jpolo1224 812ef3763b Bundle bagas' controller skins and preview every built-in one
Adds two packs from bagas as app assets, ARMSX3 Textured and ARMSX1, and makes
ARMSX3 Textured what a fresh install gets. The order offered is ARMSX3 Textured,
ARMSX2, ARMSX1, NetherSX2, NetherSX2 Old. Their files arrive named plainly
(L1.png, cross.png), which the loader already handles: it strips ic_controller_
and a trailing _button, so they are renamed to the canonical form on the way in
and both packs land on exactly the key set nethersx2 uses.

The default needed untangling first. Null meant the built-in ARMSX2 drawables
AND was what an install with no stored choice got, so the two were the same
thing and nothing had to tell them apart. They are different now. Resolution
moved into one function both callers use, because ensureLoaded and
applyForSerial each resolved separately and disagreeing would have changed the
pad the first time a game started:

  nothing stored  the default, ARMSX3 Textured
  NONE            the ARMSX2 row was chosen, so the drawables
  an id           that skin, or the default if it names one no longer installed

setActive stores NONE at the global tier rather than removing the key. Removing
it used to mean the drawables; with a bundled default it would now mean "never
chose", so picking ARMSX2 would have silently handed back ARMSX3 Textured on the
next launch. Nobody's existing choice moves: only the absence of a stored value
resolves to the default, and that is not written back, so the default can change
again later without pinning every install to today's answer.

Every built-in now shows a preview strip beside its name, matching what the
downloader already does for remote skins. Baked in as preview.png per pack
rather than composed at runtime: the list draws several at once and compositing
ten bitmaps per row per recomposition is not worth it for a picture that never
changes. keyForFilename ignores the file, so it is not counted as a button. The
ARMSX2 look has no asset folder, so its strip is a drawable.

ARMSX3 Dark was offered and is deliberately not included. It ships 17 images
against the other packs' 18, with no start button.

Verified in the APK rather than assumed: four asset packs present, 18 images
each in the two new ones, four asset previews plus the drawable.
2026-08-08 22:30:30 -04:00
jpolo1224 123b980e32 Reach All Core Settings from the in-game menu
It was reachable only from the library drawer, which is a global screen. That
put the moment a core node is actually worth touching, a game that needs one
while that game is loaded, in the one place it could not be set without setting
it for every other game too.

Add it to the Options pane beside All Settings, opening over the paused game
like the other manager screens. Scope and serial come from the overlay's own
scope state, resolved for the running title when the menu opened, so an edit
made mid-session is remembered for that title alone.
2026-08-08 22:12:39 -04:00
jpolo1224 e58b8bdb2e Scope All Core Settings edits to one game
The core settings screen recorded every edit into a single global store, so a
node set to get one title running was then set for every title that booted
afterwards, with nothing on screen to say so.

Give that store the two tiers ConfigStore already gives curated settings: a
global set, and a per-game set keyed on the same settingsKey, so both stores
agree on which title is being configured. Replay pushes global first and the
running title's set on top, so a per-game value wins where the two disagree and
every node a title never touched still follows global. With no game loaded the
per-game tier is skipped rather than guessed at, so the last title played cannot
leak into a BIOS boot.

The screen now takes its scope and serial from the caller instead of resolving
them itself: the drawer route is global and would otherwise inherit whatever
scope the last per-game settings visit left behind. It also states which tier
the next edit lands in, since the screen looks identical either way.
2026-08-08 22:12:32 -04:00
jpolo1224 8dac3c534d Add the in-app updater
Ports ARMSX2's GitHub-release updater. The UI hooks and all seventeen update.*
strings were already here from the UI port; only the implementation was missing,
so both hooks were sitting behind IN_APP_UPDATER doing nothing.

A "Check for updates" panel at the top of the App tab queries the ARMSX3
releases API, semver-compares the tag against the installed build, downloads the
.apk asset with a progress bar into externalCacheDir/updates/, and hands it to
the system package installer through a FileProvider. The user confirms the
install; nothing happens silently. Two opt-in toggles come with it, both default
off: check on launch, which pops a prompt only when something newer exists, and
include nightly builds.

Points at ARMSX2/ARMSX3 releases, not ARMSX2's. Download goes to the app cache
directory, which the OS can evict, and each download clears the folder first, so
it never accumulates.

ONE DELIBERATE DIFFERENCE FROM ARMSX2, and it needs to be understood before any
Play build exists. ARMSX2 keeps this out of its bundle with a src/github versus
src/play flavor split plus a build script that fails closed if
REQUEST_INSTALL_PACKAGES appears in the bundle manifest. ARMSX3 has neither a
Play build nor flavors, so the code, the permission and the provider live in
src/main behind the runtime flag. That is fine today and NOT fine the moment a
Play target appears: Play rejects the permission in the bundle, and a runtime
flag does not remove a permission. Adding a Play build means doing the flavor
split first. The requirement is written at the manifest, at the buildConfigField
and in the file header, because one comment is easy to miss.

Verified in the produced APK rather than assumed: the permission is present, the
provider is registered as com.armsx3.updateprovider, and UpdaterEntry is in
classes18.dex.
2026-08-08 21:56:58 -04:00
jpolo1224 97f082c2cb Wait for memory to recover before starting the next module
Demon's Souls precompiles for 27 minutes and was killed partway through. The
trajectory says why serialising alone could not save it: the process sat between
4.3GB and 5.8GB for the whole run on a 7GB device, so this was sustained
footprint rather than a transient overlap the existing mutex could flatten. It
died when the system wanted memory back, and Zygote logged signal 9 for four
processes at once, so the pressure was not ours alone.

Adds a second tier. Below 2GB free, workers already compile one at a time; below
1GB free, the worker holding that lock now waits for the system to recover before
starting the next module, up to ten seconds, rechecking every 100ms.

The wait happens AFTER taking the serialisation lock deliberately. Waiting first
would have the other worker still allocating, so the wait would be watching
memory it is not allowed to influence.

Safe to wait here because precompilation writes each object to disk and links
none of them: the modules are not mapped into the VM, so is_being_used_in_emulation
is false and no JIT instance is created. Pausing costs time and nothing else.

Bounded rather than indefinite, because failing to compile is worse than
compiling under pressure, and it gives up cleanly if the emulator is stopping.
2026-08-08 21:51:28 -04:00
jpolo1224 d7c4d4d6c0 Stop the GPU timer recording when the profiler is off
The readback in VKPresent was already gated on Video@@RSX Profiler, but
gpu_timer::begin and end were not. A release build with the profiler off, which
is the default and what everyone runs, still paid a vkCmdResetQueryPool and two
vkCmdWriteTimestamp calls per region per frame for results nothing would ever
read. Timestamp writes are not free on a tiled GPU; they are pipeline sync
points, which is the opposite of what a diagnostic should cost when disabled.

Both sides are gated now. end() deliberately clears m_open before returning
rather than bailing first: disarming between a region's begin and its end would
otherwise leave the flag set, and every later begin for that region would drop
itself as unbalanced, silently killing that region's timing for the rest of the
session. The timestamp is skipped, the bookkeeping is not.

The pool is still created at device init so arming mid-session works without a
restart. That is one small allocation, not a per-frame cost.
2026-08-08 21:45:41 -04:00
jpolo1224 08553f02d8 Keep the render pass open across attachment feedback barriers
Arkham City runs about 83 render passes a frame for roughly 339 draws, and on a
tiled GPU every pass is a tile store plus a reload of the attachment. Attributing
every end site showed where they come from:

    ImgHelper:43   37-48/frame   change_image_layout
    Barrier:inout  20-23/frame   insert_texture_barrier
    Draw:1093      13-18/frame   subpass mismatch
    Barrier:img     3-5/frame

insert_texture_barrier handles the feedback case, an attachment sampled while it
is still bound. It ended the pass because it had no choice: Vulkan forbids
vkCmdPipelineBarrier inside a render pass unless the subpass declares a
dependency on itself, and this render pass cache declared no dependencies at all.
The function already took a preserve_renderpass flag; there was simply no way for
a caller to use it legally.

So the pass now declares a by-region self-dependency, and the feedback barrier
opts in. Three parts that have to agree:

  VKRenderPass.cpp     declares the self-dependency, framebuffer-local stages
  barriers.cpp         drops the vertex stage when preserving, since
                       VK_DEPENDENCY_BY_REGION_BIT permits framebuffer-space
                       stages only and naming the vertex stage would make the
                       barrier invalid
  VKRenderTargets.cpp  passes preserve_renderpass at the cyclic-reference site

Correct for the use: the feedback case is a fragment shader sampling the
attachment its own fragments write. Anything needing vertex-stage visibility
leaves the flag false and still gets the pass ended.

Android only at the call site. The self-dependency itself is declared everywhere,
which is harmless where nothing issues an in-pass barrier.

Does not touch ImgHelper:43, the larger site. Layout transitions of
non-attachment images are illegal inside a pass whatever dependencies exist, so
that one needs resource preparation hoisted ahead of the pass instead.
2026-08-08 21:17:24 -04:00
jpolo1224 ef4ac7702e Account for every render pass end, not a third of them
Arkham City runs about 90 render passes a frame for roughly 339 draws, under four
draws per pass, on a tiled GPU where every pass costs a tile load and store. That
is worth attacking, since Fence wait is 24ms of a 53ms frame and disabling
culling to add GPU work pushed it to 43.8ms, which is what being GPU bound looks
like.

But only about 19 of those 90 ends were attributed: Draw:1093 at 16.1 and
Texture:921 at 2.9. The other 71 happened at sites with no counter, so the
report pointed at the wrong two.

Instruments the rest: the four barrier sites, both texture cache sites, the image
helper, and the occlusion query one in VKGSRender. The barrier sites are the ones
to watch, since every barrier that cannot preserve the pass ends it, and that is
the same mechanism as the Adreno leak fixed earlier: this driver does real work
on every vkCmdEndRenderPass.

Counters only. Which site dominates decides whether the fix is barrier batching,
texture cache scheduling, or something else, and guessing between those has a
poor record here.
2026-08-08 21:07:12 -04:00
jpolo1224 0ce28165e5 Stop burning a core spinning on occlusion query results
A simpleperf profile of the RSX thread during Arkham City gameplay, 83261 samples:

    24.91%  vk::query_pool_manager::get_query_result
    11.63%  [kernel]
    10.16%  rsx::nv406e::semaphore_acquire
     9.37%  rsx::FIFO::FIFO_control::fetch_u32_refill
     3.96%  memcpy_opt
     3.26%  VKGSRender::do_local_task

get_query_result is a quarter of the thread on its own, and what it does is spin:
pause(), re-poke the query, repeat, until the GPU has the result. That is a
defensible trade on a desktop, where the answer lands in microseconds and there
are cores going spare. On a tiled mobile GPU the result is not available until
the tile pass resolves, so the wait is much longer, and this device runs eleven
hot emulator threads across five usable cores. The spin does not make the result
arrive sooner; it just denies the core to an SPU thread that had work.

Keeps a short 64-iteration spin so a nearly-ready result still returns without a
scheduler round trip, then yields. Android only.

Also scopes the wait as fence_wait, which is what it is. This was completely
invisible before: the bucket report attributed 0.072ms/frame to ZCULL and showed
nothing here, because the ZCULL scope covers zcull_ctrl->update and this is
reached by another path. A bucket reading zero means no scope reached it, not
that the work is free.
2026-08-08 20:54:45 -04:00
jpolo1224 4ceefdcd08 Histogram FIFO commands by method
The per-command figure came back at 1017ns across 43870 commands per frame. That
is not a fair price for reading a word and calling a handler, so the cost is
concentrated in particular handlers rather than spread across the dispatch, and
the useful question is which.

Counts commands per method register and reports the top eight with their share,
named through gcm_printing so they read the same as the log's own FIFO traces.

A handful of methods dominating means a fast path is worth writing for them. An
even spread means the dispatch itself is the problem and this was the wrong tree.
Either way it is the last thing hidden inside fifo_decode, which now holds 44.6ms
of a 59.2ms frame with every sub-unit around it measured and small: ZCULL 0.111ms,
page protect 0.133ms, local tasks 1.357ms, the whole draw path under 6ms.

One increment behind the enabled() branch, no counter-timer read, 64KB of
counters touched only while armed.
2026-08-08 20:42:57 -04:00
jpolo1224 40ed60b2a5 Count FIFO commands, to get the per-command cost
Every sub-unit inside the RSX dispatch loop is now measured and every one is
small. ZCULL is 0.083ms/frame, page protection 0.094ms, local tasks 1.691ms,
and the entire draw path under 5ms of a 53.4ms frame. Fence wait takes 9.2ms.
That leaves 34.9ms in fifo_decode with nothing left to attribute it to except
the dispatch itself, and FIFO stalls at 1.0/frame say it is not waiting to be
fed either.

Which leaves one question worth asking: is that a lot of commands at a fair
cost each, or few commands at an unfair one. Those want opposite work. A lot of
commands means the volume is the problem and the answer is upstream of the
loop; an unfair per-command cost means the dispatch is the problem and can be
attacked directly.

So the loop counts its iterations, and the report divides fifo_decode by them.
An increment behind the enabled() branch with no counter-timer read: the
earlier attempt at a per-command SCOPE read cntvct_el0 twice per command and
took run_FIFO from 3% of samples to 35%, which is the mistake this avoids.
2026-08-08 20:35:29 -04:00
jpolo1224 19cf988c11 Split ZCULL and local tasks out of the FIFO bucket
Page protection turned out to be 0.090ms/frame, 0.2%, so the 38ms sitting in
fifo_decode is not mprotect and not the drawing either: the entire draw path
comes to under 5ms of a 54ms frame, and fence wait accounts for 8.4ms more.
Roughly 38ms had no owner.

fifo_decode is scoped around the whole RSX loop rather than around run_FIFO,
deliberately, because a scope inside run_FIFO reads the counter-timer twice per
FIFO command and previously turned a 3% bucket into 35% of samples. So anything
in that loop without a scope of its own accumulates there, and the loop's
per-64-cycle sub-unit updates are the largest unmeasured things left in it.

Both now have buckets. They run once per 64 commands, so the counter read is
amortised and the earlier distortion does not apply.

ZCULL is the specific suspect: this title logs "Reports area at location
CELL_GCM_LOCATION_MAIN was accessed. ZCULL optimizations will be disabled" and
then runs the unoptimised path for the whole session, with Accurate ZCULL stats
on, while the game polls occlusion reports.

Measurement only, no behaviour change.
2026-08-08 20:29:11 -04:00
jpolo1224 a1e5421744 Time page protection, not just count it
Arkham City under load spends 36.9ms of a 46.5ms frame in fifo_decode, which is the
bucket the FIFO loop leaves active and therefore holds everything without a scope of
its own. The drawing is not the cost: draw_setup, vertex, pipeline, descriptors,
texture upload, RT prep, blit and submit together come to under 5ms. Fence wait falls
to 5.4% under load, so it is not waiting on the GPU either.

The one number in the report large enough to explain the hole is page protection:
16.8 mprotect calls covering 40MB every frame. That is on the order of ten thousand
pages of kernel page-table work plus TLB shootdowns across eight cores, and it is
reached from RSX state handling, so every microsecond of it lands in fifo_decode.

Counting it was not enough to know whether it is most of that 36.9ms or almost none
of it, and the two point at completely different work. So it gets a bucket and a
scope at the syscall itself, charged only on the RSX thread.

No behaviour change; the scope compiles to a branch on the profiler flag, which is
off by default.
2026-08-08 20:22:21 -04:00
jpolo1224 7daa89e0e7 Give the SIGSEGV handler a stack to report from
Arkham City dies of SIGSEGV about 140ms after the game writes PS3Progress_Frame_1,
on both the Qualcomm driver and Turnip, with the database on or off, Multithreaded
RSX on or off, and the RSX profiler on or off. A table of ten runs showed no setting
correlates with it.

The reason it took so long to even establish that it WAS a segfault: nothing records
it. No tombstone (/data/tombstones is root-only, so its emptiness proves nothing), no
logcat crash-buffer entry, and no line from our own handler. The single witness
anywhere is Zygote:

    I Zygote : Process <pid> exited due to signal 11 (Segmentation fault)

That combination is what a stack overflow looks like here. The handler is installed
with SA_SIGINFO alone, so it runs on the faulting thread's own stack; if that stack is
what overflowed there is nowhere to run, it faults again immediately, and the kernel
applies the default action having written nothing.

So: an alternate signal stack per thread in thread_base::initialize, and SA_ONSTACK on
the handler. Thread-local rather than shared, because two threads can fault at once and
a shared stack would corrupt whichever report lost the race.

This does not fix the fault. It makes the fault reportable, which is the thing that has
been missing all along: the next occurrence should log where it came from instead of
vanishing. Android only.
2026-08-08 20:12:08 -04:00
jpolo1224 9d85567743 Instrument the draw path the first profile could not see
Arkham City's first RSX profile put 83.9% of a 33.45ms frame in FIFO decode and
essentially zero everywhere else. That was not a finding, it was a gap: fifo_decode
is the bucket the FIFO loop leaves active, so everything without a scope of its own
accumulates there, and draw_setup, vertex, texture_upload, shader_translate,
shader_compile, barrier and cmdbuf had no scope sites at all. They could only ever
read zero.

Adds the three that account for the draw path:

  begin()            -> draw_setup     per-draw setup, with the existing pipeline,
                                       descriptors and texcache_lookup scopes nesting
                                       inside so each is charged to itself
  emit_geometry()    -> vertex         vertex and index upload plus the draw
  bind_texture_env() -> texture_upload sampler setup and any upload the bind forces

load_program already carried a pipeline scope and reported 0.006ms/frame, so shader
and pipeline work is genuinely negligible here rather than unmeasured, and is left
alone.

What the first profile did establish stands: Present wait and Fence wait at ~0 mean
the thread is not waiting on the GPU, Idle at 15.7% means it is not starved, and
FIFO stalls at 0/frame mean it is not waiting on the guest. The 28ms is real CPU
work in the draw path. This says which part.
2026-08-08 20:06:29 -04:00
jpolo1224 65abd324b6 Compile one module at a time when memory is short
The worker count is decided once, from a reading taken before the emulator has
mapped the PS3 address space or the game has loaded anything. On a cold cache
that reading goes stale almost immediately, and by the time it matters the
count is fixed and cannot respond.

Measured on Arkham City: a first boot peaked at 5228MB where the same session
with the modules already cached sits at 2636MB, and sampling RssAnon against
RssFile and RssShmem put the growth in anon, so it is the compilers holding
LLVM contexts rather than the GPU caches. The process was killed partway
through; on a warm cache the identical settings run fine.

So the number of workers is not really the problem, their overlap is. Below
2GB free, a worker now takes a mutex around a single module's compilation,
which makes it one at a time exactly when that is the difference between
finishing and being killed. Checked per module, at the point of use, because
rechecking is the entire point. With headroom the check fails and nothing is
serialised, so there is no cost in the common case.

The lock is scoped to the compile alone, so a worker waiting on it is never
holding a context while it waits.

Android only.
2026-08-08 19:41:23 -04:00
jpolo1224 7683c9def6 Budget the PPU JIT group against memory instead of lowering it outright
Follow-up to the previous commit, which lowered modules-per-JIT to a flat 25
on Android. That fixed the kill but paid for it everywhere, including on
devices with memory to spare and on titles that were never at risk. The two
things the constant governs are not the same concern: branch reachability is
about translated game code, while the symbol resolver is a one-shot boot-time
initialiser that fills the jumptable and never runs again. Only the second is
a memory problem, so only the second should be allowed to force a split.

The group is now sized from MemAvailable, the same way the compile worker
count already is. A quarter of what is free, with the ceiling set at what a
full group of 100 costs, since budgeting past the point where nothing would
be split buys nothing.

  ~2GB free  -> 26 per JIT, resolver peak ~500MB
  ~4.4GB     -> 57 per JIT, resolver peak ~1.1GB   (this device)
  8GB+       -> 100 per JIT, i.e. upstream, untouched

Two separate reasons this stays free in the common case. A device with room
lands on 100 and is not split at all. And a title whose parts fit in one group
gets a single JIT instance at any limit at or above its part count, so every
title below the threshold is unaffected regardless: same instances, same
codegen. At roughly 4000 functions per part that covers everything under about
100k analysed functions, which is most of the library. Arkham City, at ~457k,
is not, and that is the point.

The 5KB per function and 4000 functions per part are measured off the run that
died: 2.3GB across ~457k functions, with parts holding 2800 to 4900 each.
2026-08-08 18:44:37 -04:00
jpolo1224 06a40a8637 Split the PPU JIT into smaller groups on Android
Arkham City was killed partway through compiling its PPU modules. Memory sat
level around 4GB for four minutes of ordinary compile-and-free, then went
4010MB -> 6323MB in seventeen seconds and the log stops mid-compile, no
tombstone: the kernel OOM killer on a 7GB device.

The module part that carries jit_bounds also builds the symbol resolver, and
GetSymbolResolver spans every function across its entire JIT instance rather
than its own part: an LLVM Function declaration and a constant-array entry per
function, then a relocation each through MCJIT. Arkham City analyses to about
457k functions, and at 100 modules per instance the first resolver covered all
of them at once. The log records it plainly, 457209 functions generated in one
module where every other module in the run reports between 2800 and 4900.

No new mechanism was needed. ppu_initialize already splits modules across JIT
instances and jit_mod.symbol_resolvers is already a vector with one entry per
instance, executed in a loop. The group size was simply tuned for a desktop.
Upstream's own comment on the constant names this exact trade: lowering it
lowers continuous memory requirements, at the cost of more branches unable to
reach with a direct B. The resolver's cost is linear in the functions it spans,
so a quarter of the group size is a quarter of the peak.

Android only. Desktop keeps 100.
2026-08-08 18:38:07 -04:00
jpolo1224 a49268f1a0 Stop leftover PCSX2 keys overwriting the PS3 settings they collide with
Enable Time Stretching read true on the device, though upstream defaults it
false, Settings writes ps3.audioTimeStretch which is false, and no core
override touches it. Something later was setting it back.

Six PCSX2 keys reach RPCS3 nodes that a PS3/ key already owns:

  Enable Time Stretching        <- SPU2/Output/SyncMode
  Desired Audio Buffer Duration <- SPU2/Output/OutputLatencyMS
  Enable Buffering              <- SPU2/Output/BufferMS
  Anisotropic Filter Override   <- EmuCore/GS/MaxAnisotropy
  Clocks scale                  <- Framerate/NominalScalar
  Frame limit                   <- EmuCore/GS/SyncToHostRefreshRate

They date from before the PS3/ pseudo-sections existed, which were added for
exactly this reason. Being unambiguous in the bridge was not enough: put()
calls setSetting immediately rather than collecting into a map, so applyTo's
source order is the call order, and every PS3 write lands first (L939-994)
with the PCSX2 one later (L1044-1765). The leftover won every time.

The field each UI row is bound to is the PS3 one in all six cases; the PCSX2
counterparts appear only in the reset-fields list or nowhere. So the Audio
tab's Time Stretching toggle did nothing at all: it wrote false, and SyncMode
wrote true ninety lines later.

Two were independently wrong. BufferMS turned a buffer size in milliseconds
into the boolean "buffering enabled". SyncToHostRefreshRate mapped to Frame
limit "Display", which resolves to the host panel's refresh, so on a 120Hz
handheld it asked for a 120fps cap, the same mistake just fixed in
setDisplayRefreshRate; and its `if (asBool(value))` guard could only ever set
the mode, never clear it, so turning the setting off left the cap in place.

All six now fall through to the unhandled path. Verified no RPCS3 setter is
left with more than one writer.
2026-08-08 16:58:03 -04:00
jpolo1224 5ff702f437 Stop the panel's refresh rate becoming the console's vblank
The frame rate still ran past 60. The live config on the device explains it:
Vblank Rate 120, Frame limit Auto, and Auto resolves to the vblank rate.

setDisplayRefreshRate was writing the HOST PANEL's refresh into
Video@@Vblank Rate. Those are not the same quantity. Vblank Rate is the
frequency of the emulated console's vblank, and a PS3 runs 60Hz whatever
display is attached, so a 120Hz handheld was asking the emulator for 120 frames
a second: twice the RSX command volume, twice the GPU work, for frames no PS3
game was ever written to produce.

It also could not be corrected from settings. EmulationSurface reports the panel
rate from surfaceChanged, which runs after ApplySettings on boot and again on
every rotation and resume, so it overwrote the pushed 60 every single time. That
is why recording 60 as a core override did not hold.

Dropped rather than redirected. RPCS3 reads the host rate itself through
get_display_refresh_rate() for Frame limit Display, and never wanted to be told.

Frame limit now goes through the curated push next to Vblank Rate, so the cap
does not depend on the vblank path holding. A deliberate choice on the core
screen still wins, since the overrides replay after.

Also drops the two core overrides the profiling work left behind. RSX Profiler
belongs at its false default in a build meant for playing, and Eager Surface
Readback names a node this build no longer has. Removed by path, because
clearing the store would take the user's real edits with it.
2026-08-08 16:32:09 -04:00
jpolo1224 850ce7cd4b Size the present framebuffer off the swapchain, not the request
Rotating the device left the picture corrupt in both orientations.

Two independent faults, and both have to go.

m_swapchain_dims held the size passed to swapchain::init, but the WSI backend
replaces that with the surface's currentExtent whenever the platform reports
one, so after any window reshape the two disagreed. That value is not
bookkeeping: it sizes the framebuffer the swapchain image is attached to, and
the present blit region, so a frame was drawn at one size into images of
another. Nor did it recover, because flip() decides whether to rebuild by
comparing that same stale number, so it kept confirming itself. Both init sites
now adopt the size the swapchain actually got.

The other half is that nothing told the renderer the window had changed shape.
The activity handles orientation itself, so rotation keeps the same Surface and
the same ANativeWindow, leaving no new handle to notice; and once a swapchain is
connected, ANativeWindow_getWidth answers about buffers rather than about the
window. SurfaceHolder.Callback::surfaceChanged is the one authoritative source
and it was being discarded, so it now reaches the core and drives client_width().

Three smaller things on the same path. ANativeWindow_fromSurface returns an
already acquired reference and the old code acquired again on top of it, leaking
a window on every surfaceChanged. The event code was computed after
currentSurface had been overwritten, so SURFACE_CREATED could never be sent and
the resume paired with the surface loss pause was dead code. That resume now
undoes only a pause the surface loss itself caused, so it cannot fight the
overlay's.
2026-08-08 16:32:09 -04:00
jpolo1224 82c6dcd2cd Skip conditional rendering prep when nothing can consume it
The Qualcomm driver does not expose VK_EXT_conditional_rendering, which is why
the vendor gate added earlier never fired: the feature was already off, so
turning it off changed nothing.

But VKGSRender::begin_conditional_rendering does its work regardless of support.
It allocates the predicate buffer, copies query results into it and barriers it,
then falls through to the base implementation. Only vkCmdBeginConditionalRendering
reads that buffer, so without the extension all of it is discarded.

On Adreno the waste is not merely wasted. insert_buffer_memory_barrier ends the
open render pass, and that driver allocates on every vkCmdEndRenderPass and does
not release it. A heap profile of a Skate 3 session put its largest allocation
stacks, 157MB, 152MB, 150MB and more, on exactly this path through
qglinternal::vkCmdEndRenderPass into calloc, with the process killed at 4.3GB of
anonymous memory after about 2.4GB arrived in eight seconds.

Returns to the base implementation immediately when the extension is missing.
Behaviour is unchanged, since nothing was being predicated anyway; what goes is
the buffer traffic, the barriers and the render pass ends.
2026-08-08 16:32:09 -04:00
jpolo1224 6bc5eb206c Stop using conditional rendering on the Adreno proprietary driver
A heap profile of a Skate 3 session, sampled through the crash, put every one of
the top allocation stacks on the same path:

  run_FIFO -> VKGSRender::begin -> begin_conditional_rendering
    -> insert_buffer_memory_barrier -> end_renderpass
      -> qglinternal::vkCmdEndRenderPass -> calloc

157MB, 152MB, 150MB and more from that single stack. begin_conditional_rendering
inserts a buffer memory barrier, which ends the render pass, and every
vkCmdEndRenderPass makes the Qualcomm driver allocate memory it never returns.

The process reached 4.3GB of anonymous memory, took the device to 54MB free with
3GB in swap, and was killed. About 2.4GB of it arrived in eight seconds.
Anonymous, so unreclaimable, and not ours to free: it belongs to the driver.

Without the extension RSX falls back to thread::begin_conditional_rendering,
which performs the draws instead of predicating them. Occlusion results stop
culling, which costs some GPU work, in exchange for sessions that do not end in
an OOM kill.

Scoped to the proprietary driver, which is what was measured. Turnip is a
separate implementation and is left alone until there is evidence about it.

Three earlier guesses at this, the texture cache quota, the SPU JIT and the
eager readback, were all wrong. This one came from allocation stacks.
2026-08-08 16:32:09 -04:00
jpolo1224 b82ba11b69 Budget the GPU caches against real memory, and refresh window size on rotation
Two separate fixes.

Memory: the texture and surface caches size their quotas from
device_local_total_bytes. On a discrete GPU that is right, a 3GB texture cache
out of 8GB of dedicated VRAM costs system RAM nothing. A phone has one pool for
both, so that figure is system RAM and the caches budget memory the OS also
needs. Here it reported 7446MB, which resolved the texture cache quota to
2978MB.

Watched a Skate 3 session die: steady around 1.6GB for two minutes, then 2GB
allocated in fourteen seconds, available memory down to 54MB, 3GB pushed into
swap, killed at a 4.3GB peak with no tombstone. That is the quota being honoured
on a device that cannot pay it.

Adds get_budgetable_device_memory, which is the device local heap everywhere
except Android, where it is what is actually free less room for the emulator,
clamped between 1GB and 2.5GB. Both caches now evict against that.

Rotation: getNativeWindow only re-read the window size when the window pointer
changed, and rotating keeps the same ANativeWindow while changing its
dimensions, so the cached size stayed at whatever the first orientation was and
the swapchain was rebuilt at portrait extent inside a landscape window. Size is
re-read on every query now.
2026-08-08 16:32:09 -04:00
jpolo1224 e7606bda06 Make the PPU compile worker budget survive a large title
Skate 3 still aborted in llvm::report_bad_alloc_error during PPU compilation with
4.6GB reported available, where the previous figures allowed three workers.

Both numbers were too optimistic. A single large PPU module can take well over a
gigabyte through MCJIT and relocation processing, so 1GB per worker does not
cover a big title, and the reading is taken before the emulator maps the PS3
address space, so some of what it counts is already spoken for.

Reserves 2GB for the emulator and budgets 1.5GB per worker. On a 7GB phone with
4.6GB free that is a single worker: slower to compile, but it finishes. Three was
faster right up to the point it killed the process, and a game that aborts during
compilation cannot be played at all.

Minecraft exposed this class of bug after a cache clear and Skate 3 exposed that
the first fix was not enough; neither is new, the original cap sized against
installed rather than available memory.
2026-08-08 16:32:09 -04:00
jpolo1224 671bb9b800 Keep the SPU threads off the core RSX needs most
RSX and every SPU thread shared one affinity mask, so on a 4+3+1 phone that is
six hot threads over five cores. RSX is the thread the frame waits on, and it
was measured spending about 10ms per frame inside its own loop without running:
not blocked on the GPU, not faulting, just waiting for a core.

Carves the single fastest core out of the SPU mask and leaves it in the RSX one.
RSX keeps the whole fast cluster and only loses the contention for the best
core; the SPUs lose one core out of several. Skipped when it would leave the
SPUs with a single core, which would be worse than the problem.

Only takes effect under the alternative scheduler. On Operating System mode
Android places threads itself and this code does not run.
2026-08-08 16:32:09 -04:00
jpolo1224 738da2154f Default vblank to the PS3's own 60Hz through the curated push
A PS3 runs a 60Hz vblank and every game was written against it. The stored value
was 120, which asks for twice the frames the hardware ever produced: twice the
RSX command volume, twice the vertex upload, twice the GPU work, on a handheld
chasing a panel refresh rate the games predate.

Recording it as a core override did not hold. The override is stored correctly
and the two beside it apply, but they only persist because toggling them in the
UI writes config.yml directly, and CoreSettingOverrides.replay never ran at boot
in any log taken today. So this goes through the curated push instead, which
runs on every apply.

A deliberate change in All Core Settings still wins, since the override replay
happens immediately after.
2026-08-08 16:32:09 -04:00
jpolo1224 ab3fcd735d Make thread priority actually work on Android
set_native_priority used pthread_setschedparam with sched_priority. Android
threads run under SCHED_OTHER, where sched_priority must be zero and
sched_get_priority_max returns zero, so the call succeeded and changed nothing.

The RSX thread asks for a boost when it starts and was still measured at nice 0,
taking about 5300 involuntary preemptions a second, roughly 130 per frame, from
the PPU, SPU and audio threads sharing its cores. It is the thread everything
else waits on.

Under SCHED_OTHER the scheduler weights by nice, which setpriority does set, and
Android gives apps enough RLIMIT_NICE headroom to go negative for their own
threads. Applies -8 for a raise and +8 for a drop, and warns rather than fails
if the headroom is not there.

Modest on purpose: a hint to be scheduled ahead of the other emulator threads,
not a bid to starve them.
2026-08-08 16:32:09 -04:00
jpolo1224 30124838bc Count render passes instead of timing them
Timing each render pass overran the GPU timer's per-frame event cap by two
orders of magnitude: 554600 events dropped over 300 frames, so no draw region
ever completed and the line was missing from the report entirely.

That failure is the finding. Roughly 1800 render pass begins per frame, for a
game that should need a handful. On a tiled GPU every pass boundary stores the
tile buffer to memory and reloads it, which is the most expensive thing the
architecture does, and it would account for the 20ms of GPU time on its own.

Counts them plainly instead, since a counter cannot be overrun. The blit,
upload and readback regions stay timed and are all tiny: 0.012, 0.463 and 0.144
ms per frame, so none of them is where the GPU time goes.
2026-08-08 16:32:09 -04:00
jpolo1224 903220790c Size PPU compile workers against free memory, not installed memory
Clearing the shader cache forces every PPU module to recompile at once, and the
process aborted partway through: scudo internal map failure, NO MEMORY, in
RuntimeDyldELF relocation processing on a PPU worker thread.

The existing cap allowed one worker per 1.5GB of total RAM, so four on this
device. Total RAM is the wrong number. The same device reported 7.3GB installed
while sitting at 76MB actually free, because it is also holding everything else
the user is running. Four LLVM workers on top of the emulator's own couple of
gigabytes had nowhere to go.

Adds utils::get_avail_memory, reading MemAvailable from /proc/meminfo, which is
the kernel's own estimate of what can be handed out without swapping. Workers are
then budgeted against that with a 1.5GB floor reserved for the emulator, falling
back to a more conservative slice of total where it cannot be read.

At 4.9GB available that allows three workers instead of four, and near zero it
correctly allows one.
2026-08-08 16:32:09 -04:00
jpolo1224 e1986f953b Count pipeline drains by cause
The texture cache now reports zero misses and zero hard faults, so the eager
readback is doing its job and the guest no longer faults on surfaces. Yet
submissions per frame are still around six, and GPU work plus fence wait still
sum to the whole frame, which is the serialisation keeping this at 40fps.

So the drains are not readback faults, which is what the last several changes
assumed. Counts the three remaining callers instead: GCM label release with
texture loads outstanding, the FIFO sync hint that declares a hard sync coming,
and flushes requested by another thread.
2026-08-08 16:32:09 -04:00
jpolo1224 ccbcbce360 Fetch FIFO in 4KB blocks instead of 1KB
A refill pays a fixed cost regardless of size: read_put, the iotable lookup, the
reservation lock, and a reservation_acquire per line. Measured at about 10us per
refill against roughly 1us of actual copying, so nine tenths of it was that fixed
cost, and a heavy scene ran over 1200 refills per frame to move 1.29MB of FIFO,
which is around 324,000 command words.

Fetching 4KB at a time pays that cost a quarter as often for the same bytes.

The line mask was a u8 pinned to 8 lines, so it widens to u32 with the full-mask
case special-cased, since 1u << 32 is undefined. The traversal order was checked
against the original at both widths before changing it.

Also adds counters for refill stalls, which ruled out the alternative
explanation: the retry spin is 0.006 ms/frame, so the refill is doing real work
rather than waiting on the guest.
2026-08-08 16:32:09 -04:00
jpolo1224 0f7265364c Build quad and fan index patterns once instead of per draw
The expansion indices for primitives the host cannot draw natively depend only
on the index, never on the draw's data, so the buffer for N primitives is
exactly a prefix of the buffer for any larger N. They were regenerated on every
draw call anyway, one u16 at a time, written straight into mapped GPU memory.

Builds each table once and copies the prefix. Tens of thousands of dependent
scalar stores become a single bulk copy, which also suits write-combined memory
far better than a scatter of small writes.

Minecraft draws quads throughout, so this ran on essentially every draw and
measured 5.7% of RSX thread samples.

Patterns were diffed against the original loops across edge cases before
replacing them; u16 indices bound both tables at 65536 vertices and anything
larger keeps the old path.
2026-08-08 16:32:09 -04:00
jpolo1224 1c2f13fa5a Inline the FIFO cache hit path
fetch_u32 is called once per FIFO command word and the guest pushes about 52,000
of them per frame, while the cache refills only around 208 times. Better than
99% of calls are a compare and a load, but the function lived out of line in
RSXFIFO.cpp for the sake of the rare refill, so every one of those 52,000 was a
real call into another translation unit.

It measured 15.6% of RSX thread samples, which no amount of refill work
explains: 208 refills of a 1KB cache is a few tens of microseconds of copying.
The cost was call overhead, not work.

Splits the refill into fetch_u32_refill and leaves the hit path inline.
2026-08-08 16:32:09 -04:00
jpolo1224 a8607c2fcb Stop the profiler distorting the bucket it measures
Two measurement faults, both mine, both of which sent this investigation at the
wrong targets.

The scope timing the swapchain acquire was declared at function scope, so it
lived until flip() returned and charged the whole present path against
present_wait. That bucket read 10.12 ms and looked like the largest cost in the
frame. It is now braced around the acquire alone.

The accounting was in thread locals, and under the generic TLS model every scope
enter and exit went through the linker's tlsdesc resolver: 21% of RSX thread
samples against 0.07% uninstrumented, landing hardest on the FIFO bucket, which
is why that number would not move when the FIFO fetch path was bypassed.
initial-exec removed the cost but stopped every game booting.

Since only the RSX thread is ever reported, there is now one copy of the
accounting and a thread-pointer check at the door instead of a copy per thread.
On ARM64 that is a single register read with no relocation, cheaper than one
tlsdesc access where a scope previously did six.
2026-08-08 16:32:09 -04:00
jpolo1224 d3a6269ce0 Measure GPU time by category, not just totals
CPU profiling has taken this as far as it goes. With the readback wait backed
off, the RSX thread drops to ~22% host CPU while the Adreno holds 81-88% busy at
its maximum clock and nothing throttles, so the GPU is the limiter and every
remaining question is on its side.

A whole-frame GPU timestamp would not have been worth writing: the kernel
already exposes gpu_busy_percentage and it says the same thing. What no counter
exposes is the split, specifically how much GPU time goes to copying render
targets back for the guest to read rather than to drawing. So the regions are
labelled, and readback is bracketed where the copy is actually recorded.

Readback of the queries is deferred: results come from ring slots written frames
earlier, a slot that is not ready is retried later, and no path waits on the GPU.
Reading a query in the frame that wrote it would stall on exactly the thing being
measured. vkDeviceWaitIdle is never called.

Frames are retired from flip rather than from the frame region closing, because
the primary command buffer is submitted once per flush_command_queue and several
times per frame, which would divide every per-frame figure by the wrong number.
Untimed events past the per-frame cap are reported rather than dropped silently,
so a truncated frame cannot read as a cheap one.

vkCmdWriteTimestamp had to be added to the Android Vulkan loader, which resolves
an explicit list of entry points and did not include it.
2026-08-08 16:32:09 -04:00
jpolo1224 98be68af9e Stop the readback wait from starving the GPU it waits on
vk::wait_for_event spun on vkGetEventStatus with nothing but an isb between
calls. Profiling the RSX thread put 44 to 53 percent of its samples in that
loop, on a build with no instrumentation in it, while the GPU sat at 88 percent
busy pinned at its maximum clock and nothing was thermally throttled.

A readback issued mid frame queues behind everything already submitted, so the
wait drains the whole pipeline and runs into milliseconds. Spinning through that
does not make the event arrive sooner. It holds a core at peak clock and, worse
on a tiled mobile part, keeps reading memory the GPU is writing, taking
bandwidth from the device we are blocked on.

Polls hot for a bounded window so short waits behave as before, then backs off
to 50us. Against a wait measured in milliseconds that granularity is noise.

Also pins the profiler's thread locals to initial-exec. Reading them through the
generic model routed every scope through the linker's tlsdesc resolver and cost
21 percent of RSX thread samples against 0.07 percent uninstrumented, which
inflated the FIFO bucket it was meant to measure.
2026-08-08 16:32:09 -04:00
jpolo1224 4645408eb7 Add exclusive RSX thread time accounting
The overlay's RSX percentage measures what the thread is not doing: get_load()
counts everything outside four idle sites, so a thread decoding commands and a
thread spinning on a Vulkan fence both read as fully loaded. That is exactly the
distinction that decides what is worth optimising, and it could not be read off
any existing counter.

Splits RSX thread time into exclusive buckets instead. Entering a scope charges
elapsed time to whatever was active and switches, so nesting attributes to the
innermost scope and the totals sum to wall clock rather than double counting a
caller with its callee. Reports to the log every 300 frames, including an
explicit unscoped remainder so the percentages cannot read as complete coverage
when they are not.

Behind the "RSX Profiler" video setting, dynamic, so it can be armed once a
slowdown has already started. Off by default, costing one predictable branch per
scope. Accounting is per thread because some of these paths run on whichever
guest thread faulted rather than on RSX; only the RSX thread's copy is reported.

No optimisation here, only measurement.
2026-08-08 16:32:09 -04:00
jpolo1224 3efd2a68df Let the downloaded config database be switched off and removed
Adds an apply toggle and a remove action next to the download row, so a title can
be run with and without its database entry to compare. Disabling renames the split
files aside rather than deleting them, so flipping back does not mean re-fetching
two thousand titles; remove deletes both copies and returns to stock.

Also drops Multithreaded RSX from the deny list. It was listed on the belief that
it froze Minecraft, which was wrong: the freeze was Accurate SPU DMA plus Accurate
Cache Line Stores, and it reproduced with Multithreaded RSX off and an empty
database. It is a real upstream feature backed by the RSXOffload thread, so there
was never evidence against it, and the toggle is the honest way to settle whether
a database entry helps or hurts.
2026-08-08 16:30:53 -04:00
jpolo1224 20aebe9517 Fix SPU livelock from atomic DMA cache line stores
Accurate SPU DMA and Accurate Cache Line Stores were both on. Together they
route every 128-byte SPU DMA store through do_cell_atomic_128_store, turning
bulk DMA into one atomic reservation store per cache line. Reservation
contention then outruns the rate it can drain, an SPU spins in do_putllc
forever, and the PPU stalls behind it on a semaphore the SPU never signals
while the RSX idles.

Both default off upstream. Cache Line Stores was wrong in our defaults;
SPU DMA already defaulted off but was stored on, so a default change alone
would not reach anyone who had already run the app, hence the migration.

Seen on Minecraft, which froze loading world chunks, that being bulk SPU DMA
and little else. Load dependent, so it presented as an intermittent freeze.
2026-08-08 16:30:53 -04:00
jpolo1224 25d01cd7d3 Stop routing the overlay reset through CallFromMainThread
It defers nothing on this port: the Android call_from_main_thread callback runs
the function inline on the caller's thread. All it added was RPCS3's state-guard
wrapper, a std::function allocation and a log call, on a path applyTo hits eleven
times per settings change.

The guard that skips the reset entirely while stopped is what actually prevents
the crash it was meant to fix, and that stays.
2026-08-08 16:30:53 -04:00
jpolo1224 dd7cda5ad5 Port RPCS3's config database
The recommended per-title settings the desktop build offers under "Download
Config Database". The core already knows how to use them: Emulator::Load asks
through the get_database_config callback and passes the result to BootGame as
db_config, which sits under the user's own settings.

That callback was returning a PATH, which was simply the wrong thing. The return
value is taken as the config CONTENT, so what it got back was a filename that
failed to parse and was discarded, and the feature has never done anything here.
It reads the config for the title now.

The download and the JSON parsing were the only parts missing, and they lived in
rpcs3qt, which this build does not compile. Those are done in the app instead:
api.rpcs3.net returns every title in one object, and it is split into one YAML
per title so the callback, which runs on the boot path, does not have to parse a
database of every PS3 game to find one entry.

Manual rather than automatic on launch, since it is a network call to a third
party. 2164 titles at the time of writing.
2026-08-08 16:30:53 -04:00
Lalit Shankar Chowdhury 8d034a36e8 vk: Sort enumerated GPUs according to priority 2026-08-08 18:39:16 +03:00
Lalit Shankar Chowdhury 75fea2216b Qt: Remember last used path when adding games from folder or ISO
Signed-off-by: Lalit Shankar Chowdhury <lalitshankarch@gmail.com>
2026-08-08 16:29:09 +02:00
jpolo1224 7849ea3fdc Serialise emulator teardown so boots stop failing
Emu.Kill() spawns an Emulation Join Thread that joins every emulator thread, and
nothing stopped two of those existing at once. They end up joining each other and
neither finishes, so the emulator never reaches stopped. On device that showed as
four live join threads and six leaked AudioTrack threads after a few close then
boot cycles, with the join thread logging that it was waiting on itself.

Boot took the lifecycle lock only after calling Kill, so a Close and the boot that
followed it each started a teardown and deadlocked. The lock is now taken first
and held across the whole boot, which is also what the disc probe and shutdown
already use, so probing, booting and killing can no longer overlap at all.

This is what was behind the reports of being kicked back to the library when
opening a game, of crashes when switching games quickly, and of the save screen
never appearing: all of them were the emulator failing to reach a stopped state
rather than anything wrong with booting or with saves.
2026-08-07 23:46:46 -04:00
jpolo1224 53094d3127 Stop library scanning from racing the emulator, and fix core settings persistence
Disc probing was the cause of the game switching crashes. probeDisc mounts the
image into the emulator's GLOBAL vfs to read its PARAM.SFO, vfs::mount begins
with g_fxo->need<vfs_manager>(), and the scan runs on a background thread. So a
scan overlapping a boot or a teardown aborted the process inside vfs::mount.
Closing a game does both at once: Emu.Kill() resets g_fxo and returning to the
library starts a rescan. Waiting ten seconds only worked because the scan had
finished by then.

Three parts. The probe cache is now seeded from the last scan, so a disc that
has been seen before is never mounted again, which removes almost all of the
window and stops rescans re-reading multi gigabyte images. Probing, booting and
killing take a shared lock, so what is left cannot overlap. Booting waits for the
previous VM to actually stop, since BootGame's failure path asserts IsStopped and
aborts otherwise, which is what killed PES 2018 on a failed boot.

The overlay reset that settingsSet performs is posted through CallFromMainThread
rather than run on the JNI caller's thread. It walks the overlay manager into
perf_metrics_overlay::update(), which is RSX-owned, and doing that inline is what
crashed when a setting was changed while a game was starting or closing.

All Core Settings edits are recorded and replayed at the tail of applyTo. The
curated store pushes about 165 nodes on every settings change and on every boot,
so anything set on that screen which overlapped one of them was reverted moments
later. Per-title required settings land just before them, so a game that needs a
workaround gets it while an explicit user choice still wins. Uncharted 3 needs
Stub PPU Traps to get past its own crash handler.

Also adds .rap and .edat licence installing, which routes to installKey rather
than install, and migrates the stored Scaling Mode value so PR 10's change of
meaning does not silently move everyone from Bilinear to Nearest.
2026-08-07 23:37:51 -04:00
Zulux91 c4790f8508 Stop PCSX2's duplicate frame setting from halving the framerate
SkipDuplicateFrames in PCSX2 means "do not present a frame identical to the
last one". It is harmless there and on by default, which is why Settings
defaults it to true. RPCS3 has no equivalent, and the bridge mapped it onto
Enable Frame Skip, which means something completely different: drop one frame
in every two, unconditionally.

So every game presented at half the rate the guest asked for, on a fresh
install, with nothing touched. It also made the frameskip row in the in-game
menu useless, because applyToInner pushes the explicit frameskip first and this
key afterwards, so this one always won no matter what you picked.

Measured on Mirror's Edge, which asks for 30 flips a second. SurfaceFlinger was
presenting 15.0 fps, one frame every 66.65 ms, rock steady. The RSX thread
reported 0-5% load with six of eight cores idle, which is what sent me looking
for a performance problem that was never there. With the mapping gone it sits
at 30.0 fps, 33.32 ms.

Dropped rather than remapped. Frameskip belongs to the explicit control, which
is the one the user actually set.
2026-08-07 23:09:12 -04:00
Zulux91 d9799627ff Make the Scaling Mode row actually pick the scaling mode
Four keys were writing Output Scaling Mode and the two with nothing behind them
were winning. IntegerScaling and linear_present_mode are PCSX2 keys that no
screen in this app exposes, both wrote the node unconditionally, and
IntegerScaling was emitted last, so it overwrote whatever the visible Scaling
Mode row had asked for.

On top of that the row itself was misread. It offers Nearest, Bilinear and FSR
and stores the index in casMode, but the bridge treated casMode as PCSX2's CAS
mode, where anything above zero means "CAS on". So picking Bilinear asked for
FSR, picking Nearest asked for nothing, and none of it survived anyway.

The two invisible keys no longer touch the node, and CASMode maps its own
indices. CAS is emitted before ShaderChainEnabled now so the chain still gets
the last word when both are on, which is what the bridge already documented.

casMode defaults to Bilinear rather than Nearest. Bilinear is what the core has
been using all along, and what RPCS3 itself defaults to, so nobody's picture
changes. Leaving the default at 0 would have quietly switched every user to
nearest neighbour the moment the mapping started working.

Checked on an Odin 3, reading Output Scaling Mode back out of config.yml:
picking FSR now gives FidelityFX Super Resolution where it used to give
Bilinear, and picking Bilinear gives Bilinear for the right reason.
2026-08-07 23:09:12 -04:00
Zulux91 21d95f1140 Fix console aspect ratio Auto resolving to 4:3
Closes #3.

setAspectRatio was written against ARMSX2's PS2 aspect enum, where index 1 was
4:3, and its javadoc still described that enum. ARMSX3's picker in RendererTab
is a different one: 0 stretch, 1 Auto, 2 is 4:3 and 3 is 16:9. So the check for
index 1 was matching Auto, and Auto is the default.

Every game therefore came up in 4:3 on a fresh install, and RPCS3's own default
for that node is 16:9. Setting the picker to 16:9 by hand worked, which is what
the issue reports, because index 3 fell through to the 16:9 branch.

Compare against index 2 instead, so only an explicit 4:3 selects 4:3 and
everything else lands on 16:9. That also lines it up with the other writer of
this node, writeGsToNative, which maps the same indices by name and already
treated anything other than "4:3" as widescreen.

Verified on an Odin 3: config.yml went from "Aspect ratio: 4:3" to
"Aspect ratio: 16:9" with the picker left on Auto.
2026-08-07 23:09:03 -04:00
Zulux91 599473e757 Don't let the OSD mode switch the performance overlay off at boot
Two things end up writing the same config node. RPCS3 has its own Performance
Overlay switch, in OverlayTab and the in-game menu, both writing
ps3.overlayEnabled. ARMSX2 has its twelve per-stat osdShow* flags, and
osdApplyFlags derives Enabled purely from those, all of which default to off.
So it pushed Enabled=false right over the switch.

Boot order decided the winner. MainActivityRuntime calls applyTo(), which
pushes the switch, and then applyStoredOsdMode() one line later, which pushes
the flags. Turning the overlay on in settings did nothing at all.

Only that one key was affected, because osdApplyFlags returns as soon as it
sees nothing enabled and never reaches the graph, font size and opacity
settings. That is why config.yml held the user's values for those while
Enabled sat at false.

Re-assert the switch after the flags have been applied. Scoped to the Custom
path, since that is the mode that reads saved settings and the one that runs
at boot. Full and Min pass explicit flags, and Off is meant to be off.
2026-08-07 23:09:03 -04:00
Zulux91 a4eeae8b5b Keep game data installs out of the library
A game data install looks exactly like an installed HDD game on disk, a
PARAM.SFO next to USRDIR, so isPs3GameFolder accepted it. Every title you
install data for therefore got a second tile that cannot boot, since game data
holds no EBOOT. Skate 3 ships a 1.1 GB BLUS30464_INSTALL and showed up twice.

CATEGORY is what tells them apart. GD is data, HG and DG are games. The native
scanner already rejects these, because fetchGameInfo requires BOOTABLE and game
data does not set it, but the folder path never opened the SFO at all.

Reads just that one field rather than adding a general SFO parser. Nothing else
needs it, and an unreadable or malformed file falls back to listing the folder,
so the worst case is a stray tile and never a hidden game.

ScanSchemaVersion goes up as the comment on cacheKey asks, since the cache
stores the scan result and an existing one would keep serving the old entry.
2026-08-07 23:09:03 -04:00
Zulux91 ba8947f3a3 Unmap guest memory when precompilation finishes
The precompilation queue calls vm::init() so that ppu_register_range has
somewhere to register, but nothing ever unmapped it again. vm::close() is only
reached from Emu.Stop(), and precompilation deliberately never boots anything.

So the blocks stayed mapped past "Finalization". The next vm::init(), either
the next workload or Emulator::Load() booting a game, assigns over g_locations
and destroys them, which trips ensure(!is_valid()) in ~block_t() and takes the
whole process down.

Easy to reproduce: install firmware and then boot a game without restarting the
app. The firmware pass leaves five live blocks behind and the boot dies inside
vm::ps3_::init() before it reads a byte of the disc.
2026-08-07 23:09:03 -04:00
jpolo1224 cac6590b04 Keep game and firmware art out of the gallery
Two sources, both writing real PNGs to shared storage where Android's media
scanner indexes them and they turn up in the camera roll.

The data root holds firmware assets: trophy icons under dev_hdd0/home, the whole
dev_flash VSH resource set, and an ICON0.PNG per installed game. That was 216
images. A .nomedia now goes in before the core initialises, since that is what
unpacks the firmware and creates most of them.

The ROM folder is the one people actually notice, and only since folder format
games started working. An .iso is a single opaque file so the scanner sees
nothing inside it, but a disc in folder form lays its ICON0.PNG, PIC1.PNG and
every DLC image out in the open. One Minecraft folder accounted for 245 images,
which was every image in the whole ROM tree.

The ROM marker goes at the configured directory root rather than inside a game
folder, so it covers current and future folder games with one file and never
leaves a stray file inside content that gets mounted as a disc. Writing it also
triggers a rescan, because the marker alone does not drop what MediaStore has
already indexed. Zero bytes and reversible either way.
2026-08-07 00:25:47 -04:00
jpolo1224 da6cf3bb49 Add PPU and SPU cache clearing
Two rows under a Compiled Code Cache section on the Performance tab, next to the
recompiler settings and separate from the shader cache on the Renderer tab, which
holds GPU pipelines rather than recompiled code.

The layout is <files>/cache/cache/, with ppu-<hash>-<name> directories for
firmware modules at the top level and <TITLEID>/ppu-<hash>-EBOOT.BIN per game.
The SPU cache is a spu-*.dat inside those, so the two options are not symmetric
and are worded accordingly: clearing SPU removes only those files and leaves
booting as fast as it was, while clearing PPU removes the directories outright
and necessarily takes the SPU caches with them.

Both report how much was freed, and both refuse while a game is loaded, since a
running VM holds those files open and is still writing to them.
2026-08-07 00:01:59 -04:00
jpolo1224 dbc43ec6dc Guarantee . and .. from sys_fs_opendir on host directories
fs::unix_dir::read is a bare readdir passthrough, so a listing is whatever the
host filesystem reports, in whatever order. ext4 conventionally yields . and ..
first; Android's FUSE layer over exFAT does not emit them at all. A directory
holding one file therefore left a single entry, and the stable_sort that follows
starts at data.begin() + 2, so it ran with first past last. That is undefined
behaviour rather than a no-op: it corrupted memory and the guest died later
inside libfs reading a wild pointer, with nothing at the crash site pointing back
at the cause.

Not only a crash guard. The PS3 returns both entries from opendir, so any game
walking a directory on the host filesystem was getting a listing the console
would never produce. iso_device synthesises them already, which is why the same
game booted from an .iso was unaffected and every folder format game on an SD
card was exposed. Found by booting Minecraft both ways.
2026-08-06 23:58:46 -04:00
jpolo1224 92816b9424 Fix two crashes booting folder format games
Boot audio aborted the process. init_audio does ensure() on
Emu.GetCallbacks().make_video_source(), and the Android callbacks return nullptr
because there is no media backend, so ensure() killed the app outright. It fired
for any game whose folder holds a SND0.AT3, which is every folder format game:
for an .iso the fs::is_file check in rsx::thread::thread looks inside the mounted
virtual device and never finds one, so every .iso booted so far dodged it by
accident. A null source is now handled and logged. Boot music does not play,
which it could not have anyway. PKG installed games were exposed to this too,
since they also sit on the real filesystem.

PPU compilation ran out of memory. jit_core_allocator::limit() sizes the LLVM
compile workers on core count alone, which is right on a desktop and fatal on a
handheld: eight workers on a 7 GB device aborted inside
llvm::report_bad_alloc_error partway through a large title, with Max LLVM
Compile Threads left at 0 for "use every core". The limit is now bounded by
physical memory as well, roughly one worker per 1.5 GB and never below one. It
only caps the automatic default; an explicit setting is still honoured.
2026-08-06 23:48:51 -04:00
jpolo1224 f04aa0e823 Close the progress dialog when work is complete but its text is held
The progress dialog server only leaves its loop when the counters match AND
g_progr_text is empty. That text is refcounted across nested progress scopes, so
a leaked reference leaves the loop spinning forever: the dialog is never closed,
and the cleanup that resets g_progr_ptotal never runs either, which is what
ppu_thread::cpu_task waits on before switching to overlay-message mode.

Seen on device with a fully booted, running game sitting behind a "Building SPU
Cache... 941 of 941" dialog for over ten minutes. The RSX thread and two SPU
threads were at 97%, syscall counters were climbing, and nothing had compiled
since six minutes in. Note the label is stale in that state: the server only
overwrites its cached text when it receives a non-empty one, so the last
meaningful message stays on screen and says nothing about which scope leaked.

Closes the dialog once the counters are complete and have been completely idle
for roughly five seconds. wait_no_update_count resets on any change to any
counter or to the text, so work in progress can never reach the threshold. The
warning names the held text, which is what will identify the leaking scope.

This is a safety net. The reference leak itself is still there.
2026-08-06 23:34:25 -04:00
jpolo1224 2c06abaf57 Fix aspect ratio, PKG install crash and folder boot; add split PKG and uninstall
The aspect ratio rows did nothing because Rpcs3Bridge's PS3/Video branch has an
explicit key list ending in `else -> return false`, and neither the new Display
Aspect Override nor Stretch To Display Area was in it, so both were dropped into
Unsupported.note(). Stretch had only ever reached the core through the legacy
EmuCore/GS AspectRatio key, which is why Display Mode looked inert too. Audited
the rest: all 66 PS3 keys applyTo writes are handled now. Screen aspect is also
in the in-game menu, where you can see what you are changing.

Installing a package crashed because a successful install queued the new title
for precompilation, and that path calls Emu.SetState(running), g_fxo->init<> and
vm::init(). Those are safe once, during onboarding, and not in a process that has
already booted a game. Extraction had finished by then, so the title still showed
up on the next launch. Nothing precompiles on install now; it happens on first
boot like any other game.

Installed titles booted to a black screen because BootGame was handed the game
directory. RPCSX boots them by the bootable path the installer reports, which is
the EBOOT, so directories are resolved through locateEbootPath first.

Split packages can now be installed. package_reader::extract_data always took a
deque of readers, only the entry point was single file, so a game split into
parts could not be installed at all. Select all the parts and confirm; they are
sorted by name and handed over together.

Installed titles can be uninstalled from the same screen. The path is checked
against dev_hdd0/game before anything is deleted, so it cannot touch a ROM folder.
2026-08-06 23:09:33 -04:00
jpolo1224 60aa81287f Fix first round of community reports
Settings that would not stick: the eleven Ps3Settings overlay fields were
wired into the Overlay tab but missing from all four of Settings.kt's
serialisation paths, so they only ever lived in memory and reverted the next
time the screen re-read the store. Added them to toJson, fromJson, diffFrom,
merge and the per-tab reset list.

In-game menu lag: every settingsSet ended in SaveSettings(g_cfg.to_string()),
which serialises the whole config and writes it out. applyTo pushes about 165
keys per change, so one toggle cost 165 whole-config writes on the UI thread.
Added settingsBeginBatch/settingsEndBatch and wrapped applyTo in it.

Folder format games are now detected, both the disc layout with PS3_GAME and
an installed game folder with PARAM.SFO next to USRDIR, and probeDiscInfo
reads either without mounting anything. The emulator's own dev_hdd0/game and
games directories are scanned alongside the user's ROM folders, which is why
nothing installed was ever showing up.

Package installer: the native side already handled pkg, pup, edat and iso,
but nothing in the app ever called it. Added a screen and a drawer entry.

Screen aspect ratio: the PS3 only signalled 4:3 or 16:9 so there was no way
to fill a handheld panel without stretching. Added an output aspect override
with presets and a custom slider.

All core settings screen, generated from the config tree rather than written
by hand, so nodes added upstream are editable with no app change.

Also: 176 of the 245 settings search entries still named PCSX2 settings, the
tagline still said PlayStation 2, and a missing fw.json on first run printed
a stack trace that read as a crash.
2026-08-06 21:32:13 -04:00
jpolo1224 4a3d9e322f Point the in-app links at ARMSX3 and RPCS3
What's New was reading releases from the ARMSX2 repo, and the About page
credited PCSX2 and linked to it. The navigation drawer had the same stale
GitHub link.

The website entry in the drawer still goes to armsx2.net since there is no
ARMSX3 site yet.
2026-08-06 10:02:59 -04:00
jpolo1224 5ceb0d84c1 Update README.md 2026-08-06 09:08:18 -04:00
jpolo1224 4ccd0efe75 Update README.md 2026-08-06 09:07:26 -04:00
jpolo1224 622f6306e9 Update README.md 2026-08-06 09:06:42 -04:00
Florin9doi 3d587726a2 spu: Clear the MFC_LSA_offs bits higher than the limit 2026-08-05 15:19:12 +03:00
Sanjay Govind f3f52feddf Update SDL to 3.4.14 2026-08-05 08:47:26 +02:00
474 changed files with 58131 additions and 28820 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ if [ "$DEPLOY_APPIMAGE" = "true" ]; then
# Download translations
mkdir -p "./AppDir/usr/translations"
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -45,7 +45,7 @@ if [ "$DEPLOY_APPIMAGE" = "true" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+1 -1
View File
@@ -29,7 +29,7 @@ rm -rf "rpcs3.app/Contents/Frameworks/QtPdf.framework" \
mkdir -p "rpcs3.app/Contents/translations"
ZIP_URL="https://github.com/RPCS3/rpcs3_translations/releases/latest/download/RPCS3-languages.zip"
echo "Downloading translations from: $ZIP_URL"
if curl -fsSL "$ZIP_URL" -o "translations.zip"; then
if curl -fsSL --retry 3 --retry-delay 60 "$ZIP_URL" -o "translations.zip"; then
echo "Successfully downloaded translations."
if unzip -o translations.zip -d "rpcs3.app/Contents/translations" >/dev/null 2>&1; then
rm -f translations.zip
+2 -2
View File
@@ -28,7 +28,7 @@ curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -f ISO-8859-1 -t UTF-8
# Download translations
mkdir -p ./bin/share/qt6/translations
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -36,7 +36,7 @@ if [ -z "$ZIP_URL" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+2 -2
View File
@@ -18,7 +18,7 @@ curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -t UTF-8 1> ./bin/GuiC
# Download translations
mkdir -p ./bin/qt6/translations
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -26,7 +26,7 @@ if [ -z "$ZIP_URL" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+4 -4
View File
@@ -30,23 +30,23 @@ jobs:
matrix:
include:
- os: ubuntu-24.04
docker_img: "rpcs3/rpcs3-ci-jammy:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy:2.1"
build_sh: "/rpcs3/.ci/build-linux.sh"
compiler: clang
UPLOAD_COMMIT_HASH: d812f1254a1157c80fd402f94446310560f54e5f
UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux"
- os: ubuntu-24.04
docker_img: "rpcs3/rpcs3-ci-jammy:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy:2.1"
build_sh: "/rpcs3/.ci/build-linux.sh"
compiler: gcc
- os: ubuntu-24.04-arm
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.1"
build_sh: "/rpcs3/.ci/build-linux-aarch64.sh"
compiler: clang
UPLOAD_COMMIT_HASH: a1d35836e8d45bfc6f63c26f0a3e5d46ef622fe1
UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux-arm64"
- os: ubuntu-24.04-arm
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.1"
build_sh: "/rpcs3/.ci/build-linux-aarch64.sh"
compiler: gcc
name: RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }}
+3
View File
@@ -161,3 +161,6 @@ android/**/keystore.properties
3rdparty/librashader/
android/app-upstream/
android/**/cpp/libadrenotools/
# Kotlin incremental-compile scratch dir
android/armsx3-ui/.kotlin/
+3
View File
@@ -112,3 +112,6 @@
path = 3rdparty/protobuf/protobuf
url = ../../protocolbuffers/protobuf.git
ignore = dirty
[submodule "3rdparty/oboe/oboe"]
path = 3rdparty/oboe/oboe
url = https://github.com/google/oboe.git
+15
View File
@@ -141,6 +141,12 @@ else()
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
endif()
# Oboe (Android only)
if(ANDROID)
message(STATUS "Using static oboe from 3rdparty")
add_subdirectory(oboe EXCLUDE_FROM_ALL)
endif()
# SoundTouch
add_subdirectory(SoundTouch EXCLUDE_FROM_ALL)
@@ -367,6 +373,15 @@ add_subdirectory(fusion EXCLUDE_FROM_ALL)
# FERAL INTERACTIVE
add_subdirectory(feralinteractive EXCLUDE_FROM_ALL)
# LSFG: Lossless Scaling frame generation. Android only, and deliberately NOT EXCLUDE_FROM_ALL --
# libarmsx3_lsfg.so has to be built and packaged even though nothing links it, because the core
# reaches it by dlopen rather than by linking. Marking it excluded produces a build that succeeds
# and an APK with no frame generation in it.
#
# The subdir returns immediately when the submodule is absent, so a checkout without it still
# builds; frame generation simply reports itself unavailable at runtime.
add_subdirectory(lsfg)
# add nice ALIAS targets for ease of use
if(USE_SYSTEM_LIBUSB)
add_library(3rdparty::libusb ALIAS usb-1.0-shared)
+46
View File
@@ -0,0 +1,46 @@
diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index d89a972f5d..f64e551a51 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -2504,8 +2504,40 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
RegScavenger *RS) const {
// All calls are tail calls in GHC calling conv, and functions have no
// prologue/epilogue.
- if (MF.getFunction().getCallingConv() == CallingConv::GHC)
+ if (MF.getFunction().getCallingConv() == CallingConv::GHC) {
+ // ...but they can still need an emergency spill slot.
+ //
+ // Returning here skips every path below that reserves one, so a GHC function never gets
+ // a scavenging frame index on AArch64. That is safe only while the premise holds. It
+ // stops holding as soon as the allocator spills: the function then has real stack
+ // objects, eliminateFrameIndex may need a scratch register to materialise an offset,
+ // and GHC has reserved nearly every GPR, so there is no free register to take and no
+ // slot to spill one into. The scavenger then aborts the whole module with
+ // "Cannot scavenge register without an emergency spill slot".
+ //
+ // Reproduced with RPCS3's PPU recompiler, which emits ghccc for every guest function.
+ // A single function of Saint Seiya: The Sanctuary (BLES01421) fails this way, and losing
+ // it costs the entire module, whose functions then fall back to an interpreter loop. The
+ // failure needs ghccc AND a scheduling model that pushes pressure over the line (it
+ // reproduces on cortex-x1/x2/x3 and cortex-a55, not on cortex-a76/a78/generic) AND -O2;
+ // remove any one and the same function compiles.
+ //
+ // Gated on the function actually having a frame, so a GHC function with no stack objects
+ // still gets no prologue and nothing changes for it. The cost where it does apply is one
+ // 8-byte slot.
+ MachineFrameInfo &GHCMFI = MF.getFrameInfo();
+
+ if (RS && GHCMFI.estimateStackSize(MF) > 0) {
+ const TargetRegisterInfo *TRI = MF.getSubtarget().getRegisterInfo();
+ const TargetRegisterClass &RC = AArch64::GPR64RegClass;
+ int FI = GHCMFI.CreateSpillStackObject(TRI->getSpillSize(RC), TRI->getSpillAlign(RC));
+ RS->addScavengingFrameIndex(FI);
+ LLVM_DEBUG(dbgs() << "GHC function with a frame, allocated fi#" << FI
+ << " as the emergency spill slot.\n");
+ }
+
return;
+ }
const AArch64Subtarget &Subtarget = MF.getSubtarget<AArch64Subtarget>();
+147
View File
@@ -0,0 +1,147 @@
# libarmsx3_lsfg.so -- Lossless Scaling frame generation, sealed away from the emulator core.
#
# The entire reason this is a separate shared object is symbol collision. volk defines 655 globals
# named vkCreateImage, vkQueueSubmit, ... and all 124 that our Vulkan loader declares in
# rpcs3/Emu/RSX/VK/vk_android_loader.h are among them. Linked into libarmsx3-core.so this either
# fails at link or, worse, merges -- and framegen's volkLoadDevice(itsOwnDevice) then repoints the
# whole RSX renderer at framegen's VkDevice. See armsx3_lsfg_shim.h.
#
# Android only. framegen's non-Android path shares images by FD, which Adreno and Mali refuse for
# AHB-imported memory, so there is nothing here worth building for desktop.
if (NOT ANDROID)
return()
endif()
set(LSFG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lsfg-vk-android")
if (NOT EXISTS "${LSFG_ROOT}/framegen/CMakeLists.txt")
message(STATUS "LSFG: 3rdparty/lsfg/lsfg-vk-android is missing, frame generation will not be built")
return()
endif()
if (NOT EXISTS "${LSFG_ROOT}/thirdparty/volk/volk.c")
# Called out explicitly because the failure is otherwise mystifying: framegen links volk
# PUBLIC, so without it the error names framegen rather than the submodule that is missing.
message(STATUS "LSFG: thirdparty/volk is missing (git submodule update --init), skipping")
return()
endif()
# volk, built for Android.
#
# VK_USE_PLATFORM_ANDROID_KHR has to be set on VOLK ITSELF, not only on framegen. Without it volk
# never defines vkGetAndroidHardwareBufferPropertiesANDROID, and the resulting undefined symbol
# points at framegen -- sending you to debug the wrong target entirely.
add_library(armsx3_lsfg_volk STATIC "${LSFG_ROOT}/thirdparty/volk/volk.c")
target_include_directories(armsx3_lsfg_volk PUBLIC "${LSFG_ROOT}/thirdparty/volk")
target_compile_definitions(armsx3_lsfg_volk PUBLIC VK_USE_PLATFORM_ANDROID_KHR VK_NO_PROTOTYPES)
set_target_properties(armsx3_lsfg_volk PROPERTIES
POSITION_INDEPENDENT_CODE ON
C_VISIBILITY_PRESET hidden)
# framegen.
#
# Its own CMakeLists expects a target called `volk`, so alias ours rather than patching upstream.
if (NOT TARGET volk)
add_library(volk ALIAS armsx3_lsfg_volk)
endif()
add_subdirectory("${LSFG_ROOT}/framegen" "${CMAKE_CURRENT_BINARY_DIR}/framegen" EXCLUDE_FROM_ALL)
# Undo the project-wide -fno-exceptions for framegen and the shim.
#
# The top-level build sets it with add_compile_options, which every later add_subdirectory
# inherits. framegen has dozens of throw sites and they do not warn -- they fail to compile. The
# shim needs exceptions for the opposite reason: it exists to CATCH them so none reach the dlopen
# boundary.
foreach (tgt lsfg-vk-framegen)
if (TARGET ${tgt})
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
# PRIVATE, not PUBLIC: leaking this onto consumers collides with the valueless #define
# our own Vulkan headers use, in hundreds of RSX translation units.
target_compile_definitions(${tgt} PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
endif()
endforeach()
# Shader extraction: DXBC out of the user's own Lossless.dll, translated to SPIR-V.
#
# framegen asks for SPIR-V by name and does not read the DLL itself, so this chain is the caller's
# responsibility. Building upstream's own libraries rather than writing a DXBC translator: dxbc is
# DXVK's, and reimplementing it would be absurd.
#
# Optional. Without these the library still builds and frame generation still reports itself
# available -- it just cannot initialize until shaders exist, which is also what happens when the
# user has not supplied a DLL.
set(LSFG_HAS_EXTRACT OFF)
if (EXISTS "${LSFG_ROOT}/thirdparty/dxbc/CMakeLists.txt" AND
EXISTS "${LSFG_ROOT}/thirdparty/pe-parse/CMakeLists.txt")
# pe-parse defaults to a shared library and command-line tools, neither of which belongs in
# an APK. Forced here because its options are plain option(), so they take whatever is
# already in the cache unless overridden.
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_COMMAND_LINE_TOOLS OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
add_subdirectory("${LSFG_ROOT}/thirdparty/dxbc" "${CMAKE_CURRENT_BINARY_DIR}/dxbc" EXCLUDE_FROM_ALL)
add_subdirectory("${LSFG_ROOT}/thirdparty/pe-parse" "${CMAKE_CURRENT_BINARY_DIR}/pe-parse" EXCLUDE_FROM_ALL)
foreach (tgt dxbc pe-parse)
if (TARGET ${tgt})
# Same -fno-exceptions problem as framegen: both throw, and inheriting the
# project-wide flag turns that into a compile error rather than a warning.
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden)
set(LSFG_HAS_EXTRACT ON)
endif()
endforeach()
endif()
add_library(armsx3_lsfg SHARED armsx3_lsfg_shim.cpp)
target_include_directories(armsx3_lsfg PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${LSFG_ROOT}/framegen/public")
target_compile_options(armsx3_lsfg PRIVATE -fexceptions)
target_compile_definitions(armsx3_lsfg PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
set_target_properties(armsx3_lsfg PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
OUTPUT_NAME "armsx3_lsfg")
target_link_libraries(armsx3_lsfg PRIVATE lsfg-vk-framegen armsx3_lsfg_volk android log)
if (LSFG_HAS_EXTRACT)
target_sources(armsx3_lsfg PRIVATE
"${LSFG_ROOT}/src/extract/trans.cpp"
"${LSFG_ROOT}/src/extract/extract.cpp")
target_include_directories(armsx3_lsfg PRIVATE "${LSFG_ROOT}/include")
target_link_libraries(armsx3_lsfg PRIVATE dxbc pe-parse)
target_compile_definitions(armsx3_lsfg PRIVATE ARMSX3_LSFG_HAVE_EXTRACT=1)
message(STATUS "LSFG: shader extraction enabled (dxbc + pe-parse)")
else()
message(STATUS "LSFG: shader extraction NOT available, frame generation cannot initialize")
endif()
# Keep the exported surface to the shim alone.
#
# The version script is what makes the isolation real rather than aspirational: without it,
# framegen's and volk's symbols are still dynamic and the loader can bind our renderer's vk* to
# them. Verify with:
# llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
# Only armsx3_lsfg_* may appear. Any vk* or LSFG_3_1 symbol means this stopped working.
target_link_options(armsx3_lsfg PRIVATE
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/armsx3_lsfg.map"
"-Wl,--no-undefined")
+32
View File
@@ -0,0 +1,32 @@
/* Exported surface of libarmsx3_lsfg.so.
*
* This list IS the isolation. framegen and volk are statically linked into this library and
* between them define 655 globals named vkCreateImage, vkQueueSubmit, ... -- 124 of which are
* exactly the names libarmsx3-core.so's Vulkan loader declares. If any of those stay dynamic,
* the loader is free to bind the renderer's entry points to framegen's copies, and framegen's
* volkLoadDevice() has already pointed those at a different VkDevice.
*
* -fvisibility=hidden covers most of it; this covers the rest, including anything upstream marks
* __attribute__((visibility("default"))) -- which framegen's public API does.
*
* Check it, do not assume it:
* llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
* Nothing but armsx3_lsfg_* should be listed.
*/
{
global:
armsx3_lsfg_abi_version;
armsx3_lsfg_initialize;
armsx3_lsfg_create_context_ahb;
armsx3_lsfg_present;
armsx3_lsfg_destroy_context;
armsx3_lsfg_wait_idle;
armsx3_lsfg_finalize;
armsx3_lsfg_last_error;
armsx3_lsfg_import_shaders;
armsx3_lsfg_shader_count;
armsx3_lsfg_get_shader;
local:
*;
};
+404
View File
@@ -0,0 +1,404 @@
// Implementation of the C ABI in armsx3_lsfg_shim.h.
//
// This translation unit is the ONLY thing in libarmsx3_lsfg.so that anyone outside it may touch.
// Everything else -- framegen, volk, and volk's 655 vk* globals -- stays hidden behind
// -fvisibility=hidden so the dynamic linker cannot bind our renderer's vkCmdDraw to framegen's
// copy. See the header for why that matters.
//
// Rules for every entry point here:
// * no C++ type crosses the boundary (separate libc++ per .so under c++_static),
// * no exception crosses the boundary (framegen throws; dlopen'd code must not),
// * a failure returns a code and leaves a message in armsx3_lsfg_last_error().
#include "armsx3_lsfg_shim.h"
#include <lsfg_3_1.hpp>
#include <lsfg_3_1p.hpp>
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
#include <extract/extract.hpp>
#include <extract/trans.hpp>
#include <config/config.hpp>
#endif
#include <exception>
#include <map>
#include <string>
#include <vector>
namespace
{
// thread_local because the renderer and whatever calls initialize() are not the same thread,
// and a shared buffer would let one overwrite the other's message mid-report.
thread_local std::string g_last_error;
bool g_initialized = false;
// Which shader family initialize() chose. Fixed until finalize(): LSFG_3_1 and LSFG_3_1P keep
// entirely separate device state and context tables, so a context created by one cannot be
// presented or destroyed through the other -- every entry point below has to dispatch on this.
bool g_performance = false;
void clear_error()
{
g_last_error.clear();
}
void set_error(const char* what)
{
g_last_error = what ? what : "unknown error";
}
void set_error(const std::string& what)
{
g_last_error = what.empty() ? "unknown error" : what;
}
}
// Wrap a call so nothing escapes.
//
// catch (...) rather than catching LSFG's types: framegen throws several, they are not part of
// its public header, and an exception reaching the dlopen boundary is undefined behaviour -- so
// the exact type matters less than the guarantee that none of them get out.
#define ARMSX3_LSFG_GUARD(expr, failure_result) \
try \
{ \
clear_error(); \
expr; \
} \
catch (const std::exception& e) \
{ \
set_error(e.what()); \
return (failure_result); \
} \
catch (...) \
{ \
set_error("unknown exception from framegen"); \
return (failure_result); \
}
extern "C" uint32_t armsx3_lsfg_abi_version(void)
{
return ARMSX3_LSFG_ABI_VERSION;
}
extern "C" const char* armsx3_lsfg_last_error(void)
{
return g_last_error.c_str();
}
extern "C" int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user)
{
if (!loader)
{
set_error("no shader loader supplied");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
// The std::function is built HERE, on framegen's side of the boundary, from a plain C
// function pointer. That is the whole point of taking a function pointer in the header: an
// std::function constructed by the core would be a different type under a different libc++.
//
// Throwing out of this lambda is how a missing shader is reported to framegen, which is what
// it expects -- and the throw stays inside this .so, caught by the guard below.
const auto bridge = [loader, user](const std::string& name) -> std::vector<uint8_t>
{
const uint8_t* data = nullptr;
uint32_t size = 0;
if (loader(name.c_str(), &data, &size, user) != ARMSX3_LSFG_OK || !data || !size)
{
throw std::runtime_error("shader not available: " + name);
}
return std::vector<uint8_t>(data, data + size);
};
// Recorded BEFORE the call so the guard's failure path cannot leave the two disagreeing.
g_performance = performance != 0;
if (g_performance)
{
ARMSX3_LSFG_GUARD(
LSFG_3_1P::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
else
{
ARMSX3_LSFG_GUARD(
LSFG_3_1::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
g_initialized = true;
return ARMSX3_LSFG_OK;
}
extern "C" int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
if (!in0 || !in1 || !out_n || !out_count)
{
set_error("null image or empty output set");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
int32_t id = ARMSX3_LSFG_ERR_UNKNOWN;
// AHardwareBuffer* arrives as void* so the header stays free of android/hardware_buffer.h,
// which the core has no reason to include.
std::vector<AHardwareBuffer*> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(static_cast<AHardwareBuffer*>(out_n[i]));
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1P::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
return id;
}
extern "C" int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
std::vector<int> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(out_sems ? out_sems[i] : -1);
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
return ARMSX3_LSFG_OK;
}
extern "C" int armsx3_lsfg_destroy_context(int32_t ctx)
{
if (!g_initialized)
{
return ARMSX3_LSFG_OK; // nothing to release
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
return ARMSX3_LSFG_OK;
}
extern "C" void armsx3_lsfg_wait_idle(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::waitIdle(); else LSFG_3_1::waitIdle();
}
catch (...)
{
// Deliberately swallowed and not recorded: this is called on the present path, and a
// failure to wait is reported by whatever uses the images next. Setting the error string
// here would overwrite a more useful message from the call that actually failed.
}
}
extern "C" void armsx3_lsfg_finalize(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::finalize(); else LSFG_3_1::finalize();
}
catch (...)
{
}
g_initialized = false;
}
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
// Satisfy the one symbol upstream's extract.cpp needs from its config layer.
//
// It reads exactly one field, Config::activeConf.dll, to find the file. Defining the object here
// rather than compiling their config module avoids dragging in toml11 and a config-file format
// that has no meaning inside an APK -- the path comes from the user's file picker instead.
namespace Config { Configuration activeConf; }
namespace
{
// name -> SPIR-V, translated once at import.
std::map<std::string, std::vector<uint8_t>> g_shaders;
}
extern "C" int armsx3_lsfg_import_shaders(const char* dll_path)
{
if (!dll_path || !*dll_path)
{
set_error("no file selected");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
clear_error();
g_shaders.clear();
// Upstream's own shader names, both families.
//
// Taken verbatim from nameIdxTable in extract.cpp rather than guessed -- a made-up name fails
// as "Shader hash not found", which reads like a corrupt DLL and is not.
//
// Two sets: the plain names are LSFG 3.1 and the p_ prefixed ones are 3.1p. Which family gets
// used depends on which framegen entry point runs, so both are extracted and whatever the DLL
// actually contains is kept. Missing names are skipped rather than fatal, because a given
// Lossless Scaling version legitimately ships only one family.
static const char* const k_names[] = {
"mipmaps", "alpha[0]", "alpha[1]", "alpha[2]", "alpha[3]",
"beta[0]", "beta[1]", "beta[2]", "beta[3]", "beta[4]",
"gamma[0]", "gamma[1]", "gamma[2]", "gamma[3]", "gamma[4]",
"delta[0]", "delta[1]", "delta[2]", "delta[3]", "delta[4]",
"delta[5]", "delta[6]", "delta[7]", "delta[8]", "delta[9]",
"generate",
"p_mipmaps", "p_alpha[0]", "p_alpha[1]", "p_alpha[2]", "p_alpha[3]",
"p_beta[0]", "p_beta[1]", "p_beta[2]", "p_beta[3]", "p_beta[4]",
"p_gamma[0]", "p_gamma[1]", "p_gamma[2]", "p_gamma[3]", "p_gamma[4]",
"p_delta[0]", "p_delta[1]", "p_delta[2]", "p_delta[3]", "p_delta[4]",
"p_delta[5]", "p_delta[6]", "p_delta[7]", "p_delta[8]", "p_delta[9]",
"p_generate",
};
try
{
Config::activeConf.dll = dll_path;
Extract::extractShaders();
for (const char* name : k_names)
{
// getShader hands back DXBC; framegen wants SPIR-V. Translating at import rather than
// on demand keeps the cost off the present path entirely.
//
// Individually guarded: a DLL that ships only one shader family throws on every name
// in the other, and that is normal rather than a failure of the import.
try
{
auto spirv = Extract::translateShader(Extract::getShader(name));
if (!spirv.empty())
{
g_shaders[name] = std::move(spirv);
}
}
catch (const std::exception&)
{
// Not in this DLL. Keep going.
}
}
if (g_shaders.empty())
{
set_error("no usable shaders in that file -- is it Lossless.dll from Lossless Scaling?");
return ARMSX3_LSFG_ERR_SHADERS;
}
}
catch (const std::exception& e)
{
g_shaders.clear();
set_error(e.what());
return ARMSX3_LSFG_ERR_SHADERS;
}
catch (...)
{
g_shaders.clear();
set_error("unknown failure reading the file");
return ARMSX3_LSFG_ERR_SHADERS;
}
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_shader_count(void)
{
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size)
{
if (!name || !out_data || !out_size)
{
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
const auto it = g_shaders.find(name);
if (it == g_shaders.end() || it->second.empty())
{
return ARMSX3_LSFG_ERR_SHADERS;
}
*out_data = it->second.data();
*out_size = static_cast<uint32_t>(it->second.size());
return ARMSX3_LSFG_OK;
}
#else
extern "C" int armsx3_lsfg_import_shaders(const char*)
{
set_error("this build has no shader extraction support");
return ARMSX3_LSFG_ERR_SHADERS;
}
extern "C" int armsx3_lsfg_shader_count(void) { return 0; }
extern "C" int armsx3_lsfg_get_shader(const char*, const uint8_t**, uint32_t*)
{
return ARMSX3_LSFG_ERR_SHADERS;
}
#endif
+142
View File
@@ -0,0 +1,142 @@
// C ABI for Lossless Scaling frame generation.
//
// framegen CANNOT be linked into libarmsx3-core.so. It links volk, which defines 655 globals
// named vkCreateImage, vkQueueSubmit, ... and 124 of those are byte-for-byte the names our own
// Vulkan loader declares in rpcs3/Emu/RSX/VK/vk_android_loader.h -- every single symbol the RSX
// renderer uses. Two ways that goes wrong, and the second is the one that costs a week:
//
// 1. duplicate symbol at link time (clang defaults to -fno-common), or
// 2. the linker merges them, and framegen's volkLoadDevice(itsOwnDevice) then repoints every
// entry point the renderer uses at framegen's VkDevice. Every later vkCmdDraw goes to the
// wrong device, and it presents as a driver crash with nothing pointing at frame generation.
//
// So framegen and volk live in their own libarmsx3_lsfg.so, reached by dlopen + dlsym through
// this header. Nothing here is C++: the CMake project builds ANDROID_STL=c++_static, so each .so
// carries its own libc++ and an std::vector or std::function crossing the boundary would be two
// unrelated types that happen to share a name. The shim builds those on its own side.
//
// framegen also throws (LSFG::vulkan_error and friends). Exceptions must not cross a dlopen
// boundary either, so every entry point here catches everything and returns a code; the message
// is retrievable with armsx3_lsfg_last_error().
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Bump when anything below changes shape. The loader refuses a library whose version it does not
// recognise, so a stale libarmsx3_lsfg.so on a user's device fails loudly at load instead of
// quietly passing mismatched structs.
#define ARMSX3_LSFG_ABI_VERSION 2u
// Mark the exported surface explicitly.
//
// The library is built -fvisibility=hidden so framegen's and volk's symbols stay in, and a
// version script narrows the dynamic table further. Neither of those can PROMOTE a symbol: a
// function hidden at compile time is local in the object, and `global:` in the linker script
// cannot bring it back. Without this attribute the .so builds and exports nothing at all, and
// the failure only shows up as dlsym returning null at runtime.
#if defined(__GNUC__) || defined(__clang__)
#define ARMSX3_LSFG_API __attribute__((visibility("default")))
#else
#define ARMSX3_LSFG_API
#endif
enum armsx3_lsfg_result
{
ARMSX3_LSFG_OK = 0,
ARMSX3_LSFG_ERR_UNKNOWN = -1,
ARMSX3_LSFG_ERR_NOT_INITIALIZED = -2,
ARMSX3_LSFG_ERR_BAD_ARGUMENT = -3,
ARMSX3_LSFG_ERR_SHADERS = -4,
ARMSX3_LSFG_ERR_VULKAN = -5,
};
// Hand back the SPIR-V for a named shader.
//
// framegen does NOT read Lossless.dll -- it asks for shaders by name and expects SPIR-V back.
// Extracting them from the user's own copy (PE resource -> DXBC -> SPIR-V) is the caller's job,
// which is deliberate: the shaders are THS's property and nothing here ships or downloads them.
//
// Return ARMSX3_LSFG_OK and set *out_data / *out_size on success. The buffer must stay valid
// until the initialize() call that triggered this returns. Any other return means "no such
// shader" and fails initialization.
typedef int (*armsx3_lsfg_shader_loader)(const char* name, const uint8_t** out_data,
uint32_t* out_size, void* user);
// Version of the loaded library. Call first; anything else on a mismatched library is undefined.
ARMSX3_LSFG_API uint32_t armsx3_lsfg_abi_version(void);
// Bring up framegen on the adapter identified by device_uuid (VkPhysicalDeviceIDProperties
// deviceUUID, 16 bytes, passed as the first 8 -- that is what framegen matches on).
//
// framegen creates its OWN VkDevice on that adapter. It does not share ours, which is why images
// have to be handed over as AHardwareBuffer below rather than as VkImage.
// performance selects framegen's 3.1p shader family instead of 3.1: a cheaper pipeline at lower
// quality, which is the difference between usable and not on a mobile GPU. It is fixed for the
// lifetime of the library state -- every context, present and teardown after this call goes to the
// family chosen here, because the two keep separate contexts and separate device state.
//
// flow_scale is the optical-flow resolution as a fraction of full: 1.0 is upstream's default and
// lower is cheaper. Note the sense is inverted from upstream's own config file, which stores a
// divisor and passes 1.0f/value here.
ARMSX3_LSFG_API int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user);
// Create a context over a set of shared images.
//
// AHardwareBuffer rather than the FD path framegen also offers, because Adreno and Mali both
// refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory -- the FD path simply does not work
// on the hardware this port runs on.
//
// The caller keeps ownership of every AHardwareBuffer and must keep them alive until the context
// is destroyed. Returns a context id >= 0, or a negative armsx3_lsfg_result.
ARMSX3_LSFG_API int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format);
// Generate frames for one presented pair.
//
// Semaphores are sync file descriptors, not VkSemaphore: framegen is on a different device and a
// VkSemaphore handle would be meaningless to it. in_sem is waited on before generation starts;
// each out_sems[i] is signalled when output image i is ready. Pass -1 for an unused slot.
ARMSX3_LSFG_API int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count);
ARMSX3_LSFG_API int armsx3_lsfg_destroy_context(int32_t ctx);
// Read the user's own Lossless.dll and keep the shaders it contains.
//
// Nothing is bundled or downloaded: the shaders are THS's property and the user must supply a
// legitimately purchased copy. Only the extracted SPIR-V is kept -- the DLL itself is not needed
// afterwards and the caller may delete its copy.
//
// The work is PE resource walk -> DXBC -> SPIR-V, and it is slow enough to be worth doing once
// and caching rather than at every boot. Returns the number of shaders extracted, or a negative
// armsx3_lsfg_result; armsx3_lsfg_last_error() explains a failure in terms a user can act on
// ("is Lossless Scaling up to date?" rather than a resource id).
ARMSX3_LSFG_API int armsx3_lsfg_import_shaders(const char* dll_path);
// How many shaders are currently held. Zero means frame generation cannot start.
ARMSX3_LSFG_API int armsx3_lsfg_shader_count(void);
// Serve a previously imported shader by name, for initialize()'s loader.
//
// Pass a null loader to armsx3_lsfg_initialize to use these instead of supplying your own.
ARMSX3_LSFG_API int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size);
// Block until framegen's device is idle.
//
// Needed on Android because framegen's device reads AHBs that OUR device writes, and there is no
// semaphore shared between the two. Without this the read races the write. It is also the reason
// frame generation cannot be free here: this is a device-level stall, not a queue wait.
ARMSX3_LSFG_API void armsx3_lsfg_wait_idle(void);
ARMSX3_LSFG_API void armsx3_lsfg_finalize(void);
// Message for the last failing call on this thread, or "" if none. Never null.
ARMSX3_LSFG_API const char* armsx3_lsfg_last_error(void);
#ifdef __cplusplus
}
#endif
+8
View File
@@ -0,0 +1,8 @@
# Oboe
#
# Android-only. Oboe wraps AAudio (and OpenSL ES on older devices) and carries a
# per-device quirks database plus stream-restart handling, which is the part that
# matters on the low-end parts where plain AAudio glitches.
add_subdirectory(oboe EXCLUDE_FROM_ALL)
add_library(3rdparty::oboe ALIAS oboe)
Vendored Submodule
+1
Submodule 3rdparty/oboe/oboe added at 0da326e4ef
+23
View File
@@ -12,6 +12,29 @@ project(rpcs3 LANGUAGES C CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Keep the builder's absolute paths out of the shipped binary.
#
# __FILE__ expands to whatever path the compiler was handed, and RPCS3 prints source locations in
# ensure() failures, fmt::throw_exception and assertions -- so every one of those lines carried the
# full build directory into EVERY USER'S LOG. On a developer's machine that is a home directory:
# the shipped core contained 2500 copies of one username. Someone else's crash report is not the
# place to publish where we build.
#
# -ffile-prefix-map rewrites the prefix at compile time, covering both __FILE__ (macro-prefix-map)
# and debug info (debug-prefix-map). Paths become relative-looking (./rpcs3/Emu/...), which is what
# a log wants to show anyway. Costs nothing at runtime.
#
# Applied here, before any add_subdirectory, so third-party targets built in-tree are covered too --
# they embed the same root.
if(NOT MSVC)
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-ffile-prefix-map=${CMAKE_SOURCE_DIR}=." COMPILER_HAS_FILE_PREFIX_MAP)
if(COMPILER_HAS_FILE_PREFIX_MAP)
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.>")
endif()
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)
message(FATAL_ERROR "RPCS3 requires at least gcc-13.")
+3 -37
View File
@@ -1,45 +1,13 @@
ARMSX3
======
Proof of concept Android port of RPCS3.
Uses the latest RPCS3 upstream code (the recent ARM64 improvements included).
This is early work. A game boots and plays, but it is slow and most of it is
untested. It is not a usable emulator yet.
Status
------
Skate 3 boots, loads and reaches gameplay at roughly 20 to 30 fps on a
Snapdragon 8 Gen 2. Rendering, audio, touch controls and physical controllers
work. Almost nothing else has been tested.
Differences from upstream RPCS3
-------------------------------
Some of the fixes here are not in upstream and affect any ARM64 build, not only
Android:
* Shaders declared runtime sized arrays inside uniform blocks, which requires
VK_EXT_shader_uniform_buffer_unsized_array. Adreno does not support that
extension, so every game pipeline failed to compile and nothing rendered.
Concrete array bounds are emitted when the extension is missing.
* The ARM64 SPU block verification checksum folded two thirds of every block
through an absolute difference. That collides on the near identical job
binaries an SPU job manager streams through the same local store address, so
a cached block could end up running against another job's code. It sums now.
* Thread affinity was compiled out on Android, and the core had no ARM
big.LITTLE topology, so SPU and RSX threads were never placed on the fast
cores.
* The LLVM JIT target was pinned to cortex-a34, an in order core from 2016. It
detects the host now.
Building
--------
Only arm64-v8a is supported. You need the Android SDK with NDK r27 or newer,
arm64-v8a and armv8.2 is supported. You need the Android SDK with NDK r27 or newer,
CMake 3.30 or newer, and a JDK 17. Android Studio ships all of these.
Clone with submodules, then fetch the two third party checkouts that are not
@@ -82,9 +50,7 @@ Discord's developer portal and drop it in app/libs/ and
app/src/main/cpp/discord_sdk/ if you want that feature. The build skips it
otherwise.
Running it needs PS3 firmware, which is not included. Install PS3UPDAT.PUP from
Sony's support site through the setup screen in the app.
Running it needs PS3 firmware, which is not included.
License
-------
+34 -3
View File
@@ -1,3 +1,4 @@
#include <cerrno>
#include "File.h"
#include "mutex.h"
#include "StrFmt.h"
@@ -684,8 +685,20 @@ namespace fs
u64 result = 0;
// Loop because (huge?) read can be processed partially
while (auto r = ::read(m_fd, buffer, count))
for (;;)
{
const auto r = ::read(m_fd, buffer, count);
// EINTR is benign -- a signal landed mid-syscall -- and must be retried, not treated
// as failure. Android app storage is FUSE-backed, where this genuinely happens, and
// the ensure() below turns it into a process abort. Ported in spirit from
// ouroboros420/rpcsx (92144f094).
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break; // EOF
ensure(r > 0); // "file::read"
count -= r;
result += r;
@@ -702,8 +715,17 @@ namespace fs
u64 result = 0;
// For safety; see read()
while (auto r = ::pread(m_fd, buffer, count, offset))
for (;;)
{
const auto r = ::pread(m_fd, buffer, count, offset);
// See read(): retry EINTR rather than aborting.
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break; // EOF
ensure(r > 0); // "file::read_at"
count -= r;
offset += r;
@@ -721,8 +743,17 @@ namespace fs
u64 result = 0;
// For safety; see read()
while (auto r = ::write(m_fd, buffer, count))
for (;;)
{
const auto r = ::write(m_fd, buffer, count);
// See read(): retry EINTR rather than aborting.
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break;
ensure(r > 0); // "file::write"
count -= r;
result += r;

Some files were not shown because too many files have changed in this diff Show More