74 Commits
Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
128 changed files with 10197 additions and 2581 deletions
+9
View File
@@ -373,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
+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.")
+1 -1
View File
@@ -7,7 +7,7 @@ Uses the latest RPCS3 upstream code (the recent ARM64 improvements included).
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
+402 -11
View File
File diff suppressed because it is too large Load Diff
+29 -9
View File
@@ -1803,13 +1803,6 @@ static void append_patches(patch_engine::patch_map& existing_patches, const patc
bool patch_engine::save_patches(const patch_map& patches, const std::string& path, std::stringstream* log_messages)
{
fs::file file(path, fs::rewrite);
if (!file)
{
append_log_message(log_messages, fmt::format("Failed to open patch file %s (%s)", path, fs::g_tls_error), &patch_log.fatal);
return false;
}
YAML::Emitter out;
out << YAML::BeginMap;
out << patch_key::version << patch_engine_version;
@@ -1904,7 +1897,24 @@ bool patch_engine::save_patches(const patch_map& patches, const std::string& pat
out << YAML::Flow;
out << YAML::BeginSeq;
out << fmt::format("%s", data.type);
out << fmt::format("0x%.8x", data.offset);
// move_file and hide_file carry a VFS path in the address element instead of a
// number. load() keeps that text in original_offset and skips the u32 validation for
// them, so formatting it numerically here would write out 0x00000000 and the loader
// would accept it back as a patch that silently never matches anything.
//
// The numeric branch deliberately uses 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.
if (patch_type_uses_hex_offset(data.type))
{
out << fmt::format("0x%.8x", data.offset);
}
else
{
out << data.original_offset;
}
out << data.original_value;
out << YAML::EndSeq;
}
@@ -1918,7 +1928,17 @@ bool patch_engine::save_patches(const patch_map& patches, const std::string& pat
out << YAML::EndMap;
file.write(out.c_str(), out.size());
// Write through a temporary and rename on success, as save_config already does. A truncating
// in-place write that fails part way (out of space, process killed) leaves a half-written file,
// and load() rejects the whole file on a parse error -- so a failure here costs the user every
// patch they had, with no way to rebuild it from inside the app.
fs::pending_file file(path);
if (!file.file || file.file.write(out.c_str(), out.size()) < out.size() || !file.commit())
{
append_log_message(log_messages, fmt::format("Failed to write patch file %s (%s)", path, fs::g_tls_error), &patch_log.fatal);
return false;
}
return true;
}
+3
View File
@@ -48,6 +48,9 @@ set(ARMSX3_INPUT_SOURCES
${CMAKE_SOURCE_DIR}/rpcs3/Input/mouse_gyro_handler.cpp
# Ours: on-screen touch controls.
${CMAKE_SOURCE_DIR}/rpcs3/Input/virtual_pad_handler.cpp
# Ours: cellKb fed from the Android IME / a physical keyboard. The desktop
# handler is a QObject and cannot be built here.
${CMAKE_SOURCE_DIR}/rpcs3/Input/virtual_keyboard_handler.cpp
)
add_library(rpcsx-android SHARED
+6 -3
View File
@@ -27,10 +27,13 @@ android {
defaultConfig {
applicationId = "com.armsx3"
minSdk = 26
// Set per variant by android/build-variants.sh: 33 for the A13 build (NDK 28), 35 for
// the A15 build (NDK 29). The core is compiled against the matching API, so these must
// agree -- an APK that installs below its core's target is a dlopen failure at boot.
minSdk = (project.findProperty("armsx3.minSdk") as String?)?.toInt() ?: 33
targetSdk = 37
versionCode = 11
versionName = "0.7"
versionCode = 17
versionName = "0.9.2"
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
@@ -50,3 +50,44 @@ PPU-4b46d0161ca657ab16b0a779d9062810ea5ea2dd:
- [ jumpf, 0x00000000, "RPCS3_HLE_LIBRARY:WaitForSPUsToEmptySNRs" ] # Args: (SPU ID, 3)
- [ be32, 0x00000000, 0x38800000 ] # li r4, 0
- [ be32, 0x00000000, 0x44000002 ] # sc
# Tom Clancy's H.A.W.X. 2 (BLES00928) -- boot hang at the first intro video.
#
# The SPU dies with "Access violation reading location 0x20" in CellSpursKernel0 and
# is parked forever (dbg_pause, which nothing in the Android build can clear), so the
# emulator looks healthy 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 this SPU module. The module is
# stripped -- e_shnum = 0 -- so the lookup can never succeed, on hardware either, and
# the game is built to cope: the failure path writes 0 to the work descriptor's +0x10
# field, and this very 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 the field unconditionally --
# (0 - 0x10) & ~0xF = 0xfffffff0 -- destroying the sentinel. The guard no longer
# fires, so the SPU issues GET lsa=0 ea=0 size=0x4000, a transfer that would have
# overwritten the running SPURS kernel had it succeeded.
#
# This makes the overlay routine at LS 0x3208 return immediately, which is what the
# surviving guard would have caused anyway. Safe because the section it needs cannot
# exist in a stripped module. Suppressing the DMA instead does NOT work: the guest
# loop waits on data that never arrives and runs away.
#
# Ps3PatchRepo.BUNDLED must list this, or it is imported but never enabled.
SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1:
Bink overlay skip:
Games:
"Tom Clancy's H.A.W.X. 2":
BLES00928:
- "All"
Author: Zulux91
Patch Version: 1.0
Notes: Fixes the boot hang at the first intro video.
Patch:
# LS 0x3208 is the first instruction of the overlay routine (il r5,0).
# Offsets are LS addresses: apply_modification subtracts p_vaddr (0x3000).
- [ be32, 0x3208, 0x35000000 ] # bi lr -- return immediately
@@ -98,7 +98,6 @@
"app.bgColor.rgb": "Ciclo RGB",
"app.bgColor.rgb.desc": "Desvie continuamente o fundo através do espectro de cores, como periféricos RGB. Substitui a cor fixa abaixo. Mesma limitação acima: nenhum efeito onde o plano de fundo substituto está em uso.",
"app.blockHome": "Bloquear botĂŁo Home durante o jogo",
"app.blockHome.desc": "Fixa a tela enquanto o jogo Ă© executado, para que o botĂŁo Home ou Guide do controle nĂŁo possa ser minimizado ",
"app.bootLogo": "Animação de inicialização",
"app.bootLogo.desc": "Reproduza o vídeo de introdução do ARMSX3 quando o aplicativo for iniciado.",
"app.clearCache": "Limpar dados em cache",
@@ -414,13 +413,19 @@
"overlay.uiSize.description": "Dimensiona o preenchimento do menu/biblioteca e os tamanhos de controle. 100% = padrĂŁo.",
"overlay.uiSize.label": "Tamanho da IU (bordas)",
"packages.description": "Instale um jogo, atualização ou DLC .pkg, ou um arquivo de licença .rap. Alguns jogos precisam de ambos: o .pkg contém o conteúdo e o .rap o desbloqueia. Os títulos instalados são adicionados à sua biblioteca automaticamente, e as atualizações e DLC precisam do jogo base instalado primeiro.",
"packages.install.copyFailed": "Não foi possível copiar %s desse armazenamento. Se a unidade foi desconectada ou não havia espaço suficiente para a cópia, tente novamente com o arquivo no armazenamento interno.",
"packages.install.done": "Instalado. Ele aparecerá na sua biblioteca na próxima digitalização.",
"packages.install.failed": "Falha na instalação. O arquivo pode estar criptografado, incompleto ou não ser um pacote PS3.",
"packages.install.noRoom": "Não há espaço livre suficiente para instalar %s. Esse armazenamento não pode ser lido diretamente, então o arquivo precisa ser copiado primeiro — libere espaço ou mova o arquivo para o armazenamento interno ou para um cartão SD.",
"packages.install.unreadable": "Não foi possível abrir %s. O aplicativo que fornece esse armazenamento pode ter perdido o acesso a ele — reabra a unidade e selecione o arquivo novamente.",
"packages.installed.header": "TĂ­tulos instalados",
"packages.installing": "Instalando. Pacotes grandes podem demorar alguns minutos.",
"packages.installingFile": "Instalando %s",
"packages.licences.header": "Licenças instaladas",
"packages.multiHint": "Toque em vários arquivos para selecionar todos e confirme: as partes de um jogo dividido ou um jogo junto com sua licença .rap.",
"packages.reading": "Lendo %s",
"packages.select.action": "Escolha o arquivo",
"packages.select.external": "Escolher no USB ou cartĂŁo SD",
"packages.select.title": "Selecione um arquivo .pkg ou .rap",
"packages.title": "Instalar pacote",
"packages.uninstall": "Desinstalar",
@@ -511,7 +516,6 @@
"pad.players.help": "O PS3 possui sete portas de controle e nenhum multitap, portanto, até sete pads funcionam sem configuração. Conecte-os antes do lançamento – a ordem em que eles pressionam um botão pela primeira vez é a ordem em que são atribuídos.",
"pad.pressButton": "Aperte um botĂŁo...",
"pad.pressControllerButton": "Pressione um botão do controlador…",
"pad.pressureAmount.description": "QuĂŁo forte o modificador de pressĂŁo pressiona, para jogos sensĂ­veis Ă  pressĂŁo DualShock 2 ",
"pad.pressureAmount.label": "Quantidade do modificador de pressĂŁo",
"pad.rightStick.description": "O que o botão analógico direito envia: Analógico (padrão), Face ou Personalizado (vincule cada direção abaixo).",
"pad.rightStick.invertX.description": "Espelhe o controle direito horizontalmente - corrige \"esquerda Ă© direita\".",
@@ -617,7 +621,7 @@
"perf.llvmThreads.description": "Quantos módulos PS3 são compilados ao mesmo tempo quando um jogo é inicializado pela primeira vez. Auto usa todos os núcleos da CPU, que é mais rápido, mas precisa de muita memória – o suficiente para que grandes jogos possam executar o dispositivo e fechá-lo. Reduza este valor se um jogo fechar no meio de \"Compilando Módulos PPU\".",
"perf.llvmThreads.label": "Máximo de threads de compilação LLVM",
"perf.maxSpursThreads.description": "Limita quantos encadeamentos SPURS sĂŁo executados por grupo de encadeamentos. 6 Ă© preciso em termos de hardware. Reduzi-lo Ă© um hack que pode ajudar jogos mal encadeados em dispositivos com poucos nĂşcleos, correndo o risco de quebrar outros.",
"perf.maxSpursThreads.label": "Max SPURS Threads",
"perf.maxSpursThreads.label": "Máximo de threads SPURS",
"perf.ppuDecoder.description": "Como é executada a CPU principal (PPU) do PS3. O LLVM recompila o PowerPC para o ARM64 nativo e é enormemente mais rápido - mantenha-o, a menos que você esteja depurando. O intérprete serve apenas para diagnosticar um jogo que o LLVM está errado.",
"perf.ppuDecoder.label": "Decodificador PPU",
"perf.preferredSpuThreads.description": "Quantos threads de CPU estão reservados para trabalho pesado simultâneo de SPU. Auto permite que o RPCS3 decida a partir de sua contagem de núcleos, o que geralmente ocorre em um dispositivo portátil. Definir um valor muito alto deixa o PPU sem energia.",
@@ -723,7 +727,6 @@
"renderer.clearShaderCache.alreadyEmpty": "O cache do shader já está vazio.",
"renderer.clearShaderCache.description": "Limpa os caches de shader/pipeline Vulkan + GL compilados. Use se um jogo for corrompido após uma troca ou atualização de driver – a próxima inicialização os reconstruirá de forma limpa.",
"renderer.clearShaderCache.label": "Limpar cache do sombreador",
"renderer.coalesceRenderPasses.description": "Agrupa empates consecutivos para o mesmo alvo em uma passagem de renderização. Ajuda no agrupamento de GPUs ",
"renderer.consoleAspect.description": "O aspecto que o PS3 emulado reporta ao jogo. O console sempre sinalizou 4:3 ou 16:9, então essas são as únicas opções reais - Auto deixa isso para o jogo. É para isso que o jogo serve; como ele é ajustado à SUA tela é a configuração abaixo.",
"renderer.consoleAspect.label": "Proporção do console",
"renderer.disableZcull.description": "Ignora totalmente as consultas de oclusão. Mais rápido, mas objetos que deveriam estar ocultos podem aparecer e sair - um hack de velocidade, não uma solução.",
@@ -810,8 +813,8 @@
"renderer.shaderChain.params.resetAll.confirmBody": "Cada parâmetro retorna aos padrões do próprio preset. Suas alterações aqui não podem ser desfeitas – se você quiser mantê-las, cancele e use “Salvar como nova predefinição” primeiro.",
"renderer.shaderChain.params.resetAll.confirmTitle": "Redefinir todos os parâmetros?",
"renderer.shaderChain.params.saveAs": "Salvar como nova predefinição…",
"renderer.shaderChain.pass": "passar",
"renderer.shaderChain.passes": "passes",
"renderer.shaderChain.pass": "etapa",
"renderer.shaderChain.passes": "etapas",
"renderer.shaderChain.passesUnknown": "custo desconhecido",
"renderer.shaderChain.preset.label": "Predefinição de sombreador",
"renderer.shaderChain.preset.none": "Nenhum",
@@ -877,9 +880,7 @@
"savestate.delete.title": "Excluir estado salvo",
"savestate.empty.description": "Crie um estado de salvamento enquanto o jogo está em execução e gerencie-o ou faça backup aqui.",
"savestate.empty.title": "Ainda não há estados salvos",
"savestate.error.hardcore": "Os estados de salvamento são desativados enquanto o modo Hardcore RetroAchievements está ativado. Desligue o Hardcore ",
"savestate.error.load": "NĂŁo foi possĂ­vel carregar esse slot.",
"savestate.error.memcardBusy": "O jogo ainda está gravando dados salvos, então o estado não foi salvo.",
"savestate.error.save": "NĂŁo foi possĂ­vel salvar nesse slot. Verifique o log de @@ANDROID_SAVESTATE@@.",
"savestate.hint": "Escolha um slot. Segure um slot ou use o botĂŁo de lixeira para excluĂ­-lo.",
"savestate.import": "Importar",
@@ -24,6 +24,7 @@ struct RPCSXApi {
bool (*overlayPadData)(int port, int digital1, int digital2, int leftStickX,
int leftStickY, int rightStickX, int rightStickY);
bool (*overlayPadPressure)(int port, const int *values, int count);
bool (*keyboardKey)(int androidKeyCode, int unicode, bool pressed, bool repeat);
bool (*initialize)(std::string_view rootDir, std::string_view user);
void (*setSocInfo)(std::string_view socInfo);
bool (*processCompilationQueue)(JNIEnv *env);
@@ -44,6 +45,8 @@ struct RPCSXApi {
std::string (*getCurrentTrophyName)();
bool (*surfaceEvent)(JNIEnv *env, jobject surface, jint event);
void (*surfaceSizeChanged)(int width, int height);
void (*setPadSensor)(int port, int x, int y, int z, int g);
int (*getPadRumble)(int port);
bool (*usbDeviceEvent)(int fd, int vendorId, int productId, int event);
bool (*installFw)(JNIEnv *env, int fd, long progressId);
bool (*isInstallableFile)(jint fd);
@@ -56,6 +59,9 @@ struct RPCSXApi {
std::string (*getUser)();
std::string (*settingsGet)(std::string_view path);
bool (*settingsSet)(std::string_view path, std::string_view valueString);
int (*frameGenImportShaders)(std::string_view path);
int (*frameGenShaderCount)();
const char *(*frameGenShaderError)();
void (*settingsBeginBatch)();
void (*settingsEndBatch)();
bool (*installSplitPkg)(JNIEnv *env, const int *fds, int count, long progressId);
@@ -117,6 +123,7 @@ struct RPCSXLibrary : RPCSXApi {
// clang-format off
result.overlayPadData = reinterpret_cast<decltype(overlayPadData)>(dlsym(handle, "_rpcsx_overlayPadData"));
result.overlayPadPressure = reinterpret_cast<decltype(overlayPadPressure)>(dlsym(handle, "_rpcsx_overlayPadPressure"));
result.keyboardKey = reinterpret_cast<decltype(keyboardKey)>(dlsym(handle, "_rpcsx_keyboardKey"));
result.initialize = reinterpret_cast<decltype(initialize)>(dlsym(handle, "_rpcsx_initialize"));
result.setSocInfo = reinterpret_cast<decltype(setSocInfo)>(dlsym(handle, "_rpcsx_setSocInfo"));
result.processCompilationQueue = reinterpret_cast<decltype(processCompilationQueue)>(dlsym(handle, "_rpcsx_processCompilationQueue"));
@@ -136,6 +143,8 @@ struct RPCSXLibrary : RPCSXApi {
result.getCurrentTrophyName = reinterpret_cast<decltype(getCurrentTrophyName)>(dlsym(handle, "_rpcsx_getCurrentTrophyName"));
result.surfaceEvent = reinterpret_cast<decltype(surfaceEvent)>(dlsym(handle, "_rpcsx_surfaceEvent"));
result.surfaceSizeChanged = reinterpret_cast<decltype(surfaceSizeChanged)>(dlsym(handle, "_rpcsx_surfaceSizeChanged"));
result.setPadSensor = reinterpret_cast<decltype(setPadSensor)>(dlsym(handle, "_rpcsx_setPadSensor"));
result.getPadRumble = reinterpret_cast<decltype(getPadRumble)>(dlsym(handle, "_rpcsx_getPadRumble"));
result.usbDeviceEvent = reinterpret_cast<decltype(usbDeviceEvent)>(dlsym(handle, "_rpcsx_usbDeviceEvent"));
result.installFw = reinterpret_cast<decltype(installFw)>(dlsym(handle, "_rpcsx_installFw"));
result.isInstallableFile = reinterpret_cast<decltype(isInstallableFile)>(dlsym(handle, "_rpcsx_isInstallableFile"));
@@ -147,6 +156,12 @@ struct RPCSXLibrary : RPCSXApi {
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
result.settingsSet = reinterpret_cast<decltype(settingsSet)>(dlsym(handle, "_rpcsx_settingsSet"));
// Resolved without ensure(): a core built before frame generation existed simply has no such
// symbol, and refusing to load it over a missing optional feature would be worse than the
// feature being absent. The Kotlin side treats a null here as "unsupported".
result.frameGenImportShaders = reinterpret_cast<decltype(frameGenImportShaders)>(dlsym(handle, "_rpcsx_frameGenImportShaders"));
result.frameGenShaderCount = reinterpret_cast<decltype(frameGenShaderCount)>(dlsym(handle, "_rpcsx_frameGenShaderCount"));
result.frameGenShaderError = reinterpret_cast<decltype(frameGenShaderError)>(dlsym(handle, "_rpcsx_frameGenShaderError"));
result.settingsBeginBatch = reinterpret_cast<decltype(settingsBeginBatch)>(dlsym(handle, "_rpcsx_settingsBeginBatch"));
result.settingsEndBatch = reinterpret_cast<decltype(settingsEndBatch)>(dlsym(handle, "_rpcsx_settingsEndBatch"));
result.installSplitPkg = reinterpret_cast<decltype(installSplitPkg)>(dlsym(handle, "_rpcsx_installSplitPkg"));
@@ -250,6 +265,20 @@ extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadPressure(
return ok;
}
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_keyboardKey(
JNIEnv *, jobject, jint androidKeyCode, jint unicode, jboolean pressed,
jboolean repeat) {
// Absent on a core older than this export. Returning false is right either
// way: it means "nothing consumed this key", which is also what an emulator
// with no keyboard attached reports.
if (rpcsxLib.keyboardKey == nullptr) {
return false;
}
return rpcsxLib.keyboardKey(androidKeyCode, unicode, pressed == JNI_TRUE,
repeat == JNI_TRUE);
}
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_initialize(
JNIEnv *env, jobject, jstring rootDir, jstring user, jstring socInfo) {
// The core is dlopen()ed separately and may not be up yet -- during
@@ -428,6 +457,24 @@ extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_surfaceEvent(
return rpcsxLib.surfaceEvent(env, surface, event);
}
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_setPadSensor(
JNIEnv *, jobject, jint port, jint x, jint y, jint z, jint g) {
if (rpcsxLib.setPadSensor == nullptr) {
return;
}
rpcsxLib.setPadSensor(port, x, y, z, g);
}
extern "C" JNIEXPORT jint JNICALL Java_net_rpcsx_RPCSX_getPadRumble(
JNIEnv *, jobject, jint port) {
if (rpcsxLib.getPadRumble == nullptr) {
return 0;
}
return rpcsxLib.getPadRumble(port);
}
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_surfaceSizeChanged(
JNIEnv *, jobject, jint width, jint height) {
if (rpcsxLib.surfaceSizeChanged == nullptr) {
@@ -1011,3 +1058,23 @@ Java_net_rpcsx_RPCSX_getRsxThreadTid(JNIEnv *, jobject) {
}
return static_cast<jint>(rpcsxLib.getRsxThreadTid());
}
extern "C" JNIEXPORT jint JNICALL
Java_net_rpcsx_RPCSX_frameGenImportShaders(JNIEnv *env, jobject, jstring path) {
if (!rpcsxLib.frameGenImportShaders) {
return -1;
}
return rpcsxLib.frameGenImportShaders(unwrap(env, path));
}
extern "C" JNIEXPORT jint JNICALL
Java_net_rpcsx_RPCSX_frameGenShaderCount(JNIEnv *, jobject) {
return rpcsxLib.frameGenShaderCount ? rpcsxLib.frameGenShaderCount() : 0;
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_frameGenShaderError(JNIEnv *env, jobject) {
const char *msg = rpcsxLib.frameGenShaderError ? rpcsxLib.frameGenShaderError() : "";
return env->NewStringUTF(msg ? msg : "");
}
@@ -469,6 +469,21 @@ object CustomCovers {
(target.isFile && target.length() > 0L).also { if (it) version.value++ }
}.getOrDefault(false)
/**
* Follow a game's custom cover across an identity correction.
*
* The file is named after the serial, so a game whose id is corrected stops matching its
* own cover -- and because [remove] resolves through the same name, the orphan cannot be
* deleted from the app either. Skips when a cover already exists under the new id, so a
* deliberate choice is never overwritten by a stale one.
*/
fun renameSerial(context: Context, old: String, new: String): Boolean = runCatching {
val from = File(dir(context), sanitize(old) + ".png")
val to = File(dir(context), sanitize(new) + ".png")
if (!from.isFile || to.exists()) return@runCatching false
from.renameTo(to).also { if (it) version.value++ }
}.getOrDefault(false)
fun remove(context: Context, game: GameInfo): Boolean {
val f = fileFor(context, game) ?: return false
return f.delete().also { if (it) version.value++ }
@@ -171,12 +171,18 @@ object Ps3PatchRepo {
*
* appVersion is carried for symmetry with [Patch]; the native side matches on
* serial and ignores it.
*
* sinceRevision is the [BUNDLED_REVISION] this entry first shipped in. It is what
* keeps a bump from touching the patches that were already here: an install whose
* stored revision is at or above it has been offered this patch once already, and
* whatever the user did with the toggle afterwards is their answer.
*/
private data class Bundled(
val hash: String,
val name: String,
val serial: String,
val appVersion: String,
val sinceRevision: Int,
)
private val BUNDLED = listOf(
@@ -187,6 +193,16 @@ object Ps3PatchRepo {
name = "Graphics Fix",
serial = "BLUS30008",
appVersion = "01.01",
sinceRevision = 1,
),
// Tom Clancy's H.A.W.X. 2, BLES00928 -- without this the game hangs forever at
// the first intro video with a dead SPU. See canary_patches.yml.
Bundled(
hash = "SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1",
name = "Bink overlay skip",
serial = "BLES00928",
appVersion = "All",
sinceRevision = 2,
),
)
@@ -197,7 +213,7 @@ object Ps3PatchRepo {
* install re-imports and enables the new ones. Not a timestamp: it has to be
* something a diff of this file makes obvious.
*/
private const val BUNDLED_REVISION = 1
private const val BUNDLED_REVISION = 2
private const val PREFS_NAME = "ARMSX2"
private const val KEY_BUNDLED_REVISION = "ps3_bundled_patch_revision"
@@ -210,9 +226,10 @@ object Ps3PatchRepo {
* tick a box before Sonic '06 renders has already concluded the emulator is
* broken.
*
* Guarded by a stored revision rather than run every boot, so turning one OFF
* sticks. Re-enabling on every launch would make the toggle look broken, which
* is the same class of bug as not having the patch at all.
* Only patches newer than the stored revision are touched, so turning one OFF
* sticks -- including across a later bump made for some other game. Re-enabling
* on every launch, or on every bump, would make the toggle look broken, which is
* the same class of bug as not having the patch at all.
*
* Safe to call on every boot: it is a preference read once the revision matches,
* and the import itself merges rather than replaces, so a downloaded database
@@ -220,7 +237,21 @@ object Ps3PatchRepo {
*/
fun ensureBundledPatches(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
if (prefs.getInt(KEY_BUNDLED_REVISION, 0) >= BUNDLED_REVISION) return
val storedRevision = prefs.getInt(KEY_BUNDLED_REVISION, 0)
if (storedRevision >= BUNDLED_REVISION) return
// Anything at or below the stored revision has had its one chance to be turned
// on. Re-enabling it here would silently undo a user's OFF, and patch_config.yml
// stores "disabled" as an absent entry, so there is nothing to read back that
// would tell us the difference between "opted out" and "never seen".
val pending = BUNDLED.filter { it.sinceRevision > storedRevision }
if (pending.isEmpty()) {
// Nothing new to enable, so skip the import entirely rather than rewriting
// patches/patch.yml for no reason.
prefs.edit().putInt(KEY_BUNDLED_REVISION, BUNDLED_REVISION).apply()
return
}
val yaml = runCatching {
context.assets.open(BUNDLED_ASSET).bufferedReader().use { it.readText() }
@@ -241,8 +272,10 @@ object Ps3PatchRepo {
// Only mark the revision done if every patch actually turned on. A failure
// here means the hash or name drifted from the YAML, and retrying next boot
// is better than silently shipping a game that does not render.
val allEnabled = BUNDLED.all { b ->
// is better than silently shipping a game that does not render. The retry
// covers only `pending`, so a patch that is stuck failing cannot drag the
// already-settled ones back on every boot with it.
val allEnabled = pending.all { b ->
val ok = runCatching {
RPCSX.instance.patchSetEnabled(b.hash, b.name, b.serial, b.appVersion, true)
}.getOrDefault(false)
@@ -254,7 +287,7 @@ object Ps3PatchRepo {
if (allEnabled) {
prefs.edit().putInt(KEY_BUNDLED_REVISION, BUNDLED_REVISION).apply()
android.util.Log.i("ARMSX3", "canary patches: imported $imported, enabled ${BUNDLED.size}")
android.util.Log.i("ARMSX3", "canary patches: imported $imported, enabled ${pending.size}")
}
}
}
@@ -0,0 +1,456 @@
package com.armsx2
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.armsx2.data.library.ParamSfo
import net.rpcsx.RPCSX
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
/**
* Imports PS3 save data into `config/dev_hdd0/home/<user>/savedata/` from a SAF-picked folder or
* archive.
*
* This exists because of a platform rule, not a bug of ours. Android 11 blocks third-party file
* managers from writing into `Android/data/<pkg>/`, so a user who downloads a roster or a save
* cannot put it where the emulator reads from: ZArchiver reports `EACCES (Permission denied)` and
* there is no way round it from outside the app. Reported against All Pro Football 2K8 on an Ayn
* Thor Pro. We are the only process that can still write there, so the copy has to happen in here.
*
* The destination folder name comes from the save's own PARAM.SFO, not from what the user's folder
* or archive happened to be called. That is the whole reliability argument for this class. Games
* enumerate saves by matching `dirNamePrefix` against the directory name (cellSaveData.cpp:543), so
* a save placed under the wrong name is not an error the user ever sees -- the game simply reports
* no save data and offers to start fresh, which looks like the import silently did nothing. The
* core writes SAVEDATA_DIRECTORY into every PARAM.SFO it saves (cellSaveData.cpp:1695) and reads it
* back to populate dirName (cellSaveData.cpp:248), so the correct name travels inside the save.
*
* Follows [TexturePackInstaller] for staging and commit: everything lands in a scratch directory on
* the same filesystem, is validated there, and only then is renamed into place. Nothing half-formed
* is ever visible under `savedata/`, and a failure part-way cannot destroy a save the user already
* had. The pieces here that are not savedata-specific -- [stageArchive], [stageTree], [commit] --
* are what the frame-generation plugin installer needs too (pick a file, verify it, atomically
* place it somewhere the app owns); they are written to be lifted rather than reimplemented.
*/
object SaveDataImporter {
private const val TAG = "SaveDataImporter"
/** Guards against a decompression bomb: real save data is kilobytes to a few megabytes. */
private const val MAX_ENTRY_BYTES = 256L * 1024 * 1024
private const val MAX_TOTAL_BYTES = 1024L * 1024 * 1024
private const val MAX_ENTRIES = 20_000
sealed interface Progress {
data object Scanning : Progress
data class Copying(val done: Int, val total: Int) : Progress
data object Installing : Progress
}
/** One save found in the source, named as it will actually be written. */
data class Imported(val dirName: String, val title: String?, val replaced: Boolean)
data class Outcome(
val ok: Boolean,
val saves: List<Imported> = emptyList(),
val error: String? = null,
)
// ---- entry points ---------------------------------------------------------------------
/**
* Imports from a `.zip` picked with `ActivityResultContracts.OpenDocument`.
*
* Blocking; call from a background dispatcher.
*/
fun importArchive(
context: Context,
uri: Uri,
onProgress: (Progress) -> Unit = {},
isCancelled: () -> Boolean = { false },
): Outcome = runImport(onProgress) { staging ->
// Opened separately rather than with `?.use { } ?: openFailed`. These stages answer null
// to mean "no problem, carry on", so folding them together made the SUCCESS path -- a null
// from stageArchive -- select the elvis branch and report every single archive import as
// "could not open the selected file", while the staged files were discarded unread.
val input = runCatching { context.contentResolver.openInputStream(uri) }.getOrNull()
?: return@runImport Outcome(false, error = "Could not open the selected file")
input.use { stageArchive(it, staging, onProgress, isCancelled) }
}
/**
* Imports from a folder picked with `ActivityResultContracts.OpenDocumentTree`.
*
* Accepts either the save folder itself or a parent holding several, since a user who
* downloaded a pack of rosters has no reason to know which of those they picked.
*/
fun importFolder(
context: Context,
treeUri: Uri,
onProgress: (Progress) -> Unit = {},
isCancelled: () -> Boolean = { false },
): Outcome = runImport(onProgress) { staging ->
val root = DocumentFile.fromTreeUri(context, treeUri)
?: return@runImport Outcome(false, error = "Could not open the selected folder")
stageTree(context, root, staging, onProgress, isCancelled)
}
// ---- shared driver --------------------------------------------------------------------
/**
* Stages, validates, then commits. [stage] does only the copy; it must not touch the live
* savedata directory, which is what makes a cancelled or failed import a no-op.
*/
private fun runImport(
onProgress: (Progress) -> Unit,
stage: (File) -> Outcome?,
): Outcome {
val savedataRoot = savedataRoot() ?: return Outcome(
false,
error = "No user profile yet — boot a game once, then import.",
)
// A sibling of the destination, so the commit below is a rename and not a copy across
// filesystems. Leading dot keeps it out of the way of anything that lists savedata/.
val staging = File(savedataRoot, ".import-tmp")
staging.deleteRecursively()
if (!staging.mkdirs()) {
return Outcome(false, error = "Could not create a staging folder")
}
try {
onProgress(Progress.Scanning)
stage(staging)?.let { return it }
val found = discover(staging)
if (found.isEmpty()) {
return Outcome(
false,
error = "No save data found. A save is a folder containing PARAM.SFO.",
)
}
onProgress(Progress.Installing)
val imported = mutableListOf<Imported>()
for ((staged, dirName) in found) {
val dest = File(savedataRoot, dirName)
val replaced = dest.exists()
if (!commit(staged, dest)) {
return Outcome(
false,
imported,
"Could not write $dirName into the savedata folder",
)
}
imported += Imported(
dirName = dirName,
title = ParamSfo.string(File(dest, "PARAM.SFO"), "TITLE"),
replaced = replaced,
)
}
return Outcome(true, imported)
} catch (e: Exception) {
Log.w(TAG, "import failed: ${e.message}")
return Outcome(false, error = e.message ?: "Import failed")
} finally {
staging.deleteRecursively()
}
}
// ---- discovery and naming --------------------------------------------------------------
/**
* Finds every staged directory holding a PARAM.SFO, paired with the name it must be written
* under. That is the same test the core uses to decide a directory is a save at all: it loads
* `<entry>/PARAM.SFO` per directory when enumerating (cellSaveData.cpp:240).
*
* Searched recursively because the source shape is not ours to dictate -- a user may hand us
* the save, its parent, or an archive that wraps both in a download folder.
*/
private fun discover(staging: File): List<Pair<File, String>> {
val out = mutableListOf<Pair<File, String>>()
fun walk(dir: File, depth: Int) {
if (depth > 6) return
if (File(dir, "PARAM.SFO").isFile) {
resolveDirName(dir)?.let { out += dir to it }
// A save has no nested saves; stopping also stops a PARAM.SFO in a subfolder from
// being imported as a second, bogus save.
return
}
dir.listFiles().orEmpty().filter { it.isDirectory }.forEach { walk(it, depth + 1) }
}
walk(staging, 0)
return out
}
/**
* The directory name to write this save under: PARAM.SFO's SAVEDATA_DIRECTORY when it has one,
* else the folder's own name.
*
* Preferring the SFO is what makes a renamed download still work. Names look like
* `<SERIAL><TAG>` (`BLUS30760SM2011_SAVE`), which is not something a user can be expected to
* reconstruct after their file manager or a zip tool has flattened or renamed a folder.
*
* The fallback is not a formality: a save copied by hand out of another emulator may have had
* its SFO rewritten. Both paths go through [sanitizedDirName] because a value read out of a
* file is untrusted input no matter which file it came from.
*/
private fun resolveDirName(dir: File): String? {
val fromSfo = ParamSfo.string(File(dir, "PARAM.SFO"), "SAVEDATA_DIRECTORY")
return sanitizedDirName(fromSfo) ?: sanitizedDirName(dir.name)
}
/**
* A directory name safe to join onto the savedata root.
*
* Rejects rather than repairs. A name carrying a separator or a `..` is not a name we can
* correct into the user's intent, and quietly writing it somewhere else would be worse than
* saying so: this is the value that decides where the copy lands.
*/
private fun sanitizedDirName(raw: String?): String? {
val name = raw?.trim().orEmpty()
if (name.isEmpty() || name == "." || name == "..") return null
if (name.length > 64) return null
if (name.any { it == '/' || it == '\\' || it < ' ' }) return null
// Deliberately NOT narrowed to a character set. This name comes from the game's own
// SAVEDATA_DIRECTORY, and rejecting one for holding a character we did not anticipate
// would refuse a good save with "no save data found" -- the silent-looking failure this
// whole class exists to avoid. Only separators and control characters can redirect a
// write, and a leading dot would make a directory no file browser shows.
if (name.startsWith('.')) return null
return name
}
// ---- staging: archive ------------------------------------------------------------------
/**
* Extracts [input] into [staging].
*
* Entry paths are rebuilt from sanitized components rather than used as given. A crafted
* `../../lib/foo.so` would otherwise be written wherever the app can reach, and the app can
* reach its own native library directory -- so this is a code-execution path, not a tidiness
* one. Any entry containing a `..` component fails the whole archive: an archive carrying one
* is not an archive to half-extract and then trust.
*/
private fun stageArchive(
input: InputStream,
staging: File,
onProgress: (Progress) -> Unit,
isCancelled: () -> Boolean,
): Outcome? {
val stagingCanonical = staging.canonicalPath + File.separator
var entries = 0
var totalBytes = 0L
var written = 0
ZipInputStream(input.buffered()).use { zip ->
while (true) {
if (isCancelled()) return Outcome(false, error = null)
val entry: ZipEntry = zip.nextEntry ?: break
try {
if (++entries > MAX_ENTRIES) {
return Outcome(false, error = "Archive has too many files")
}
if (entry.isDirectory) continue
val rel = safeRelativePath(entry.name)
?: return Outcome(false, error = "Archive contains an unsafe path")
if (rel.isEmpty() || isJunk(entry.name)) continue
val out = File(staging, rel)
// Belt and braces. safeRelativePath already dropped every `..`, so reaching
// this is a bug in it rather than a crafted archive -- but the cost of the
// check is nothing and the cost of being wrong is arbitrary file write.
if (!out.canonicalPath.startsWith(stagingCanonical)) {
Log.w(TAG, "zip-slip entry rejected: ${entry.name}")
return Outcome(false, error = "Archive contains an unsafe path")
}
out.parentFile?.mkdirs()
var entryBytes = 0L
FileOutputStream(out).use { fos ->
val buf = ByteArray(64 * 1024)
while (true) {
if (isCancelled()) return Outcome(false, error = null)
val n = zip.read(buf)
if (n < 0) break
entryBytes += n
totalBytes += n
// Sizes are checked while writing, not from the entry header: the
// header is attacker-controlled and can simply lie.
if (entryBytes > MAX_ENTRY_BYTES || totalBytes > MAX_TOTAL_BYTES) {
return Outcome(false, error = "Archive is unexpectedly large")
}
fos.write(buf, 0, n)
}
}
written++
if (written % 16 == 0) onProgress(Progress.Copying(written, 0))
} finally {
zip.closeEntry()
}
}
}
if (written == 0) return Outcome(false, error = "Archive was empty")
return null
}
/**
* Rebuilds an entry path from its own components, keeping only the basename of each.
*
* Every component is reduced to its last path-ish token and anything left that is `.` or `..`
* is dropped, so no combination of separators, doubled slashes or backslashes can climb out of
* the staging directory. Depth is capped because the structure a save needs is at most a
* folder and its files.
*/
private fun safeRelativePath(name: String): String? {
val norm = name.replace('\\', '/')
val parts = norm.split('/')
.map { it.trim() }
.filter { it.isNotEmpty() && it != "." }
if (parts.any { it == ".." }) return null
if (parts.isEmpty()) return ""
// Drop leading wrappers so a "Download/BLUS30760SAVE/PARAM.SFO" still stages usefully;
// discover() walks anyway, so this only keeps the tree shallow.
// Control characters only. Stripping spaces here silently renamed the user's folders,
// and a wrapper like "All Pro Football 2K8 roster/" is a completely ordinary thing for
// a file manager to produce.
val kept = parts.takeLast(3).map { part -> part.filterNot { c -> c < ' ' } }
if (kept.any { it.isEmpty() }) return null
return kept.joinToString("/")
}
// ---- staging: folder -------------------------------------------------------------------
/** Copies a picked SAF tree into [staging], mirroring its structure. */
private fun stageTree(
context: Context,
root: DocumentFile,
staging: File,
onProgress: (Progress) -> Unit,
isCancelled: () -> Boolean,
): Outcome? {
var copied = 0
var totalBytes = 0L
fun walk(node: DocumentFile, dest: File, depth: Int): Outcome? {
if (depth > 6) return null
for (child in node.listFiles()) {
if (isCancelled()) return Outcome(false, error = null)
val rawName = child.name ?: continue
// The picker gives us display names, which are not path components; a name with a
// separator in it is malformed and is dropped rather than joined.
if (rawName.any { it == '/' || it == '\\' || it < ' ' }) continue
if (rawName == "." || rawName == "..") continue
if (isJunk(rawName)) continue
if (child.isDirectory) {
val sub = File(dest, rawName)
if (!sub.exists() && !sub.mkdirs()) continue
walk(child, sub, depth + 1)?.let { return it }
continue
}
val out = File(dest, rawName)
out.parentFile?.mkdirs()
context.contentResolver.openInputStream(child.uri)?.use { input ->
FileOutputStream(out).use { fos ->
val buf = ByteArray(64 * 1024)
while (true) {
val n = input.read(buf)
if (n < 0) break
totalBytes += n
if (totalBytes > MAX_TOTAL_BYTES) return@use
fos.write(buf, 0, n)
}
}
}
if (totalBytes > MAX_TOTAL_BYTES) {
return Outcome(false, error = "Folder is unexpectedly large")
}
copied++
if (copied % 16 == 0) onProgress(Progress.Copying(copied, 0))
}
return null
}
// The picked folder may itself be the save, so its own name has to survive into staging or
// the dirName fallback would see the scratch directory instead.
val rootName = root.name?.takeIf { n ->
n.none { it == '/' || it == '\\' || it < ' ' } && n != "." && n != ".."
}
val base = if (rootName != null) File(staging, rootName).also { it.mkdirs() } else staging
walk(root, base, 0)?.let { return it }
if (copied == 0) return Outcome(false, error = "Folder contained no files")
return null
}
// ---- commit ----------------------------------------------------------------------------
/**
* Moves [staged] to [target], keeping any existing save until the new one is in place.
*
* Overwriting matters more here than for a texture pack: the thing being replaced is the
* user's own progress, and a rename that fails half way must leave what they had rather than
* nothing at all.
*/
private fun commit(staged: File, target: File): Boolean {
val backup = File(target.parentFile, "${target.name}.old-import")
backup.deleteRecursively()
target.parentFile?.mkdirs()
val hadPrevious = target.exists()
if (hadPrevious && !target.renameTo(backup)) {
Log.w(TAG, "could not move existing ${target.name} aside")
return false
}
if (!staged.renameTo(target)) {
if (hadPrevious) backup.renameTo(target)
Log.w(TAG, "could not move staged ${target.name} into place")
return false
}
backup.deleteRecursively()
return true
}
// ---- paths -----------------------------------------------------------------------------
/**
* `config/dev_hdd0/home/<user>/savedata`, created if the user directory already exists.
*
* Prefers the logged-in user and falls back to whichever home directory is actually there,
* matching how the trophy browser resolves the same ambiguity: getUser() reaches through JNI
* into the core and answers null before a game has been opened, and refusing to import until
* then would be a confusing rule to explain. Answers null only when there is no user directory
* at all, which is a genuinely fresh install.
*/
private fun savedataRoot(): File? {
val home = File(RPCSX.getHdd0Dir(), "home")
val preferred = runCatching { RPCSX.instance.getUser() }.getOrNull()
?.takeIf { it.isNotBlank() }
val user = preferred
?.let { File(home, it) }
?.takeIf { it.isDirectory }
?: home.listFiles().orEmpty()
.filter { it.isDirectory && it.name.length == 8 && it.name.all(Char::isDigit) }
.minByOrNull { it.name }
?: return null
return File(user, "savedata").also { it.mkdirs() }.takeIf { it.isDirectory }
}
private fun isJunk(name: String): Boolean {
val lower = name.lowercase()
return lower.startsWith("__macosx/") || lower.contains("/__macosx/") ||
lower == "__macosx" || lower.endsWith("/.ds_store") || lower == ".ds_store" ||
lower.endsWith("thumbs.db")
}
}
@@ -63,6 +63,8 @@ object ConfigStore {
private const val KEY_SPU_DECODER_RESTORE = "config.migrated.spuDecoderRestoreLlvm"
private const val KEY_XFLOAT_BACK_TO_APPROX = "config.migrated.xfloatBackToApprox"
private const val KEY_PRECISE_SPU_OFF = "config.migrated.preciseSpuVerifyOff"
// Oboe became the Android default in 0.7.2; move anyone still on the old Cubeb default.
private const val KEY_AUDIO_OBOE = "config.migrated.audioOboeDefault"
private const val KEY_ATOMIC_DMA_OFF = "config.migrated.atomicDmaStoresOff"
// Bumped: the first pass recorded only Vblank Rate, which did not hold on its own.
private const val KEY_VBLANK_60 = "config.migrated.frameCap60"
@@ -71,10 +73,18 @@ object ConfigStore {
// Bumped: the profiler was recorded again during the 0.5 debugging work, after the first
// purge had already marked itself done.
private const val KEY_DIAG_OVERRIDES_PURGED_2 = "config.migrated.diagOverridesPurged2"
private const val KEY_SHADOWING_OVERRIDES_PURGED = "config.migrated.shadowingOverridesPurged"
// Core settings left pinned as raw overrides by the 0.5 debugging sessions.
private const val KEY_TUNING_OVERRIDES_PURGED = "config.migrated.tuningOverridesPurged"
// Per-title Accurate SPU Reservations values left behind by the same debugging.
private const val KEY_PERGAME_RSV_CLEARED = "config.migrated.perGameRsvCleared"
// The GLOBAL Accurate SPU Reservations value left off by the same debugging. The per-title
// clear above never touched it, so installs carried an off-spec global for releases.
private const val KEY_GLOBAL_RSV_ON = "config.migrated.globalSpuRsvOn"
// "Save LLVM logs", left on while chasing the Saint Seiya register scavenger. Bumped: the
// first pass only un-pinned the override, which does nothing for a key no code writes -- the
// value already in config.yml is reloaded and saved again on every boot.
private const val KEY_LLVM_LOGS_OFF_2 = "config.migrated.llvmLogsOff2"
private const val KEY_RELAXED_ZCULL_ON = "config.migrated.relaxedZcullOn"
private const val KEY_RELAXED_ZCULL_OFF = "config.migrated.relaxedZcullOff"
// The relaxed-ZCULL default was recorded as a raw core override as well, and the OFF
@@ -107,7 +117,20 @@ object ConfigStore {
private const val BACKUP_FILENAME = "armsx2-settings.json"
private fun keyForGame(serial: String) = "config.game.$serial"
// Memoized result of loadGlobal(). The function below is a JSON parse plus every
// migration block in this file -- 24,555 dex instructions by ART's count, over its
// JIT ceiling, so it runs interpreted every single call. That would be fine if it
// were called rarely, but EmulationSurface's frame-rate monitor re-resolves the
// config every 5 seconds of gameplay (measured: one ART bailout log line per 5.00s
// for entire sessions), all to read one boolean. The migrations are one-shot by
// their own prefs flags, so caching the parsed result is behavior-identical; the
// cache is refreshed by saveGlobal (the only writer of KEY_GLOBAL after boot) and
// dropped by reconcileReusedFolder, whose restore writes the pref directly.
@Volatile
private var cachedGlobal: Settings? = null
fun loadGlobal(): Settings {
cachedGlobal?.let { return it }
val raw = MainActivityRuntime.prefs.getString(KEY_GLOBAL, null)
var parsed = if (raw != null) {
try { Settings.fromJson(JSONObject(raw)) } catch (_: Exception) { Settings() }
@@ -316,6 +339,16 @@ object ConfigStore {
}
// Oboe is the Android default now. Only move people sitting on the previous default
// (Cubeb, index 2) -- anyone who deliberately picked Null or another backend keeps it.
if (!MainActivityRuntime.prefs.getBoolean(KEY_AUDIO_OBOE, false)) {
if (raw != null && parsed.ps3.audioRenderer == 2) {
parsed = parsed.copy(ps3 = parsed.ps3.copy(audioRenderer = 4))
dirty = true
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_AUDIO_OBOE, true) }
}
// The ARM64 block checksum is fixed, so the full-compare workaround can go.
if (!MainActivityRuntime.prefs.getBoolean(KEY_PRECISE_SPU_OFF, false)) {
if (raw != null && parsed.ps3.preciseSpuVerification) {
@@ -357,6 +390,55 @@ object ConfigStore {
MainActivityRuntime.prefs.edit { putBoolean(KEY_ATOMIC_DMA_OFF, true) }
}
// Put the GLOBAL Accurate SPU Reservations back on, which is upstream's default and this
// app's default too.
//
// Off is not a slower-but-correct trade, it is off-spec: it forces the SPURS scheduler to
// HLE and bypasses the reservation lock, so SPU threads desync and end up executing
// whatever they land on. See KEY_PERGAME_RSV_CLEARED below, which cleared the PER-TITLE
// values this same debugging left behind -- but never the global, so an install kept
// running off-spec no matter what any title said.
//
// Found via Borderlands 2 hanging after its logo with one SPURS SPU and the RSX pinned
// while every PPU sat in a legitimate wait. The same desync is the likeliest source of the
// wild guest register values that were crashing the process before that.
if (!MainActivityRuntime.prefs.getBoolean(KEY_GLOBAL_RSV_ON, false)) {
if (raw != null && !parsed.ps3.accurateSpuRsv) {
parsed = parsed.copy(ps3 = parsed.ps3.copy(accurateSpuRsv = true))
dirty = true
}
// Both stores, because either alone is not enough: a raw core override is re-pushed
// after the settings themselves, so one left recorded would put false straight back
// over the line above. KEY_TUNING_OVERRIDES_PURGED cleared these once already, but it
// marks itself done, so anything recorded afterwards survived it.
//
// Global scope ONLY, deliberately. This is correcting the baseline everyone inherited,
// not overruling a per-title decision -- Web of Shadows (BLUS30218) is kept off on
// purpose, and forgetEverywhere() would take that with it.
runCatching {
CoreSettingOverrides.forget(SettingsScope.Global, null, "Core@@Accurate SPU Reservations")
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_GLOBAL_RSV_ON, true) }
}
// Drop "Save LLVM logs", left pinned as a raw override while chasing the Saint Seiya
// register-scavenger failure.
//
// Upstream defaults it off and this app has no field or UI for it, so nothing would ever
// turn it back off again -- it writes the IR for every compiled module to disk on every
// boot, which costs compile time and a lot of storage for output nobody is reading.
// Recorded as false rather than merely un-pinned, and that distinction is the whole fix:
// forgetting an override only stops us re-pushing a value, and nothing in this app writes
// this key at all, so whatever is already in config.yml is simply reloaded and saved again
// forever. It has to be actively written off, the way the Vblank migration writes 60.
if (!MainActivityRuntime.prefs.getBoolean(KEY_LLVM_LOGS_OFF_2, false)) {
runCatching {
CoreSettingOverrides.record(SettingsScope.Global, null, "Core@@Save LLVM logs", "false")
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_LLVM_LOGS_OFF_2, true) }
}
// Put Vblank Rate back to 60, which is both upstream's default and what a PS3
// actually runs at.
//
@@ -441,6 +523,61 @@ object ConfigStore {
MainActivityRuntime.prefs.edit { putBoolean(KEY_DIAG_OVERRIDES_PURGED_2, true) }
}
// Drop raw overrides on nodes a curated settings screen also writes.
//
// These two cannot coexist. Overrides replay at the tail of applyTo, after the curated
// store has written the same node, so the recorded value wins every time and the normal
// 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. A test device carried
// Core@@PPU Decoder = "Recompiler (LLVM)" this way, which silently defeated every
// attempt to boot a game on the interpreter -- including one run specifically to find
// out whether a hang was a codegen bug.
//
// Named rather than derived: the curated set is spread across applyToInner and the
// Rpcs3Bridge routing table, and a wrong automatic answer here would delete real user
// edits. Every path in the first group is reachable from Settings, so nothing is lost --
// the value still applies, it just comes from the screen that shows it.
//
// Video@@Accurate ZCULL stats is deliberately NOT purged: it has no curated writer and
// no debugging history, so a recorded value there is most likely a deliberate per-game
// performance choice. It is visible and clearable in All Core Settings now instead.
//
// The two migrations above purged diagnostics by name and both had already run on the
// device that still had RSX Profiler recorded, which is why All Core Settings now shows
// and clears overrides directly instead of waiting for the next migration.
if (!MainActivityRuntime.prefs.getBoolean(KEY_SHADOWING_OVERRIDES_PURGED, false)) {
runCatching {
CoreSettingOverrides.forgetEverywhere(
"Core@@PPU Decoder",
"Core@@SPU Decoder",
"Core@@SPU XFloat Accuracy",
"Core@@Max SPURS Threads",
"Core@@Precise SPU Verification",
"Core@@PPU Vector NaN Handling",
"Video@@Shader Mode",
"Video@@Multithreaded RSX",
)
// These three have no curated writer, so forgetting alone would leave the
// recorded value sitting in config.yml with nothing to overwrite it -- the
// record would be gone and the effect would remain, which is worse than
// leaving it. Write the core's own default off instead, the way the Vblank
// migration writes 60 rather than deleting.
//
// All three are instrumentation or debug levers, off by default upstream:
// the RSX profiler keeps per-scope timers on the RSX thread and reports every
// 300 frames, PPU calling history records every call, and the GETLLAR spin
// optimization being disabled changes how an SPU waiting on a reservation
// behaves -- which is not something to ship switched off by accident.
CoreSettingOverrides.record(SettingsScope.Global, null, "Video@@RSX Profiler", "false")
CoreSettingOverrides.record(SettingsScope.Global, null, "Core@@PPU Calling History", "false")
CoreSettingOverrides.record(
SettingsScope.Global, null, "Core@@Disable SPU GETLLAR Spin Optimization", "false",
)
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_SHADOWING_OVERRIDES_PURGED, true) }
}
// Move anyone still on the old Approximate xfloat default onto Accurate.
// Approximate corrupted SPU float registers badly enough that a job
// manager built a DMA command out of one; see Settings.spuXFloat. A
@@ -555,6 +692,7 @@ object ConfigStore {
}
if (dirty) saveGlobal(parsed)
cachedGlobal = parsed
return parsed
}
@@ -574,6 +712,7 @@ object ConfigStore {
fun saveGlobal(s: Settings) {
MainActivityRuntime.prefs.edit { putString(KEY_GLOBAL, s.toJson().toString()) }
cachedGlobal = s
writeBackupMirror()
}
@@ -758,6 +897,10 @@ object ConfigStore {
// Hard guard: an existing new-UI user (has config.global) is off-limits.
if (MainActivityRuntime.prefs.getString(KEY_GLOBAL, null) != null) return
// The restore below writes KEY_GLOBAL behind loadGlobal's back; drop any
// default Settings() a pre-restore call may have pinned in the cache.
cachedGlobal = null
// (1) Lossless restore from the in-folder mirror (written by a prior new-UI install).
val mirror = backupFile()
if (mirror != null && mirror.exists() && mirror.length() > 0L) {
@@ -126,6 +126,14 @@ data class Ps3Settings(
* wait for their real shader instead of running through the interpreter.
*/
val shaderMode: Int = 1,
/** Lossless Scaling frame generation: 0 Off, 1 x2, 2 x3, 3 x4. Off unless the user has
* supplied shaders from their own copy -- nothing is bundled. */
val frameGeneration: Int = 0,
// Default ON: 3.1p is the cheaper of the two shader families framegen ships, and on a mobile
// GPU the full-quality path costs more than the frames it buys.
val frameGenPerformance: Boolean = true,
// Optical-flow resolution as a percentage of full; lower is cheaper and blurrier in motion.
val frameGenFlowScale: Int = 100,
val writeColorBuffers: Boolean = false,
val writeDepthBuffer: Boolean = false,
val readColorBuffers: Boolean = false,
@@ -198,6 +206,28 @@ data class Ps3Settings(
* preciseSpuVerification). Accurate is a rarely-exercised path and costs
* speed, so there is no reason to sit on it.
*/
/**
* Which face button confirms in PS3 system dialogs. 0 = circle, 1 = cross, matching
* enter_button_assign. Japanese-region games and hardware confirm with circle; the rest of
* the world uses cross, which is why RPCS3 exposes it rather than deriving it from region.
*/
val enterButtonAssign: Int = 1,
/**
* The rest of the console's identity, as cellSysutil reports it to games: language, region,
* keyboard layout and clock formats.
*
* All five are an INDEX into the tables in Rpcs3Settings, not the core's enum value, and the
* bridge turns them into the enum NAME the config expects. Defaults match upstream --
* English (US), SCEA, US keyboard, ddmmyyyy, clock24 -- so an existing install is unchanged.
*
* A game reads these: the language decides which text a multi-language disc shows, and the
* region is what makes a title behave as its NTSC or PAL self.
*/
val consoleLanguage: Int = 1,
val consoleRegion: Int = 1,
val keyboardType: Int = 0,
val dateFormat: Int = 1,
val timeFormat: Int = 1,
val spuXFloat: Int = 1,
val accurateSpuRsv: Boolean = true,
/**
@@ -249,7 +279,8 @@ data class Ps3Settings(
val debugConsoleMode: Boolean = false,
val resolution: Int = 2,
val anisoFilter: Int = 0,
val audioRenderer: Int = 2,
/** Index into Rpcs3Settings.AUDIO_RENDERERS. 4 = Oboe, the Android default (see node_audio). */
val audioRenderer: Int = 4,
/**
* Output aspect override in permille (1778 = 16:9, 1333 = 4:3), 0 = follow the game.
*
@@ -271,10 +302,17 @@ data class Ps3Settings(
val overlayPosition: Int = 0,
// RPCS3 stores these as "#RRGGBBAA" strings. Kept as packed ARGB ints here so
// the existing colour picker can drive them, and converted on the way out.
val overlayBodyColor: Int = 0xFFE138FF.toInt(),
val overlayBodyBg: Int = 0x002339FF,
val overlayTitleColor: Int = 0xF26C24FF.toInt(),
val overlayTitleBg: Int = 0x00000000,
// ARGB, because that is what Android colour ints are and what argbToRgba() converts FROM.
//
// These used to hold RPCS3's RGBA hex values verbatim (0xFFE138FF and friends), which are the
// right colours in the wrong order: argbToRgba then read the leading FF as alpha and rotated
// every channel one byte left, turning the default orange #FFE138FF into #E138FFFF. That is
// the pink the overlay has always drawn in, and it made the colour pickers look broken --
// every value the user chose was rotated the same way, so nothing ever matched.
val overlayBodyColor: Int = 0xFFFFE138.toInt(), // core #FFE138FF
val overlayBodyBg: Int = 0xFF002339.toInt(), // core #002339FF
val overlayTitleColor: Int = 0xFFF26C24.toInt(), // core #F26C24FF
val overlayTitleBg: Int = 0x00000000, // core #00000000, fully transparent
)
data class Settings(
@@ -698,12 +736,18 @@ data class Settings(
val memoryCardSlot2Enabled: Boolean = true,
val memoryCardSlot2Filename: String = "mcd002.ps2",
// ---- USB ----
/** USB1/Type = hidkbd — attach an emulated USB HID keyboard on USB port 1.
* Needed by games that require a real USB keyboard (EverQuest Online
* Adventures, Konami-keyboard titles). A physical/Bluetooth keyboard's key
* events are forwarded to it (see MainActivityRuntime.dispatchKeyEvent → NativeApp.usbKeyboardKey).
* Default off. */
// ---- Keyboard ----
/** Input/Output/Keyboard = Basic — serve cellKb from the Android keyboard handler.
* Needed by games that want a keyboard (EverQuest Online Adventures, in-game
* text chat, the debug menus some titles put behind one). Keys come from a
* physical/Bluetooth keyboard (MainActivityRuntime.forwardKeyToUsbKeyboard) or
* from the Android IME the On-Screen Keyboard hotkey raises (SoftKeyboard), and
* reach the core through NativeApp.usbKeyboardKey.
*
* The name is ARMSX2's. RPCS3 has no emulated USB HID keyboard device; it has a
* keyboard handler, which is what this drives.
*
* Read once, in Emulator::Load, so it takes effect on the next boot. Default off. */
val usbKeyboard: Boolean = false,
// ---- EmuCore/CPU/Recompiler — recompiler enables ----
@@ -1041,6 +1085,9 @@ data class Settings(
put("PS3/Overlay", "Title Background (hex)", "string", argbToRgba(ps3.overlayTitleBg))
put("PS3/Video", "MSAA", "enum", ps3.msaaMode.toString())
put("PS3/Video", "Shader Mode", "enum", ps3.shaderMode.toString())
put("PS3/Video", "Frame Generation", "enum", ps3.frameGeneration.toString())
put("PS3/Video", "Frame Generation Performance Mode", "bool", ps3.frameGenPerformance.toString())
put("PS3/Video", "Frame Generation Flow Scale", "int", ps3.frameGenFlowScale.toString())
put("PS3/Video", "Write Color Buffers", "bool", ps3.writeColorBuffers.toString())
put("PS3/Video", "Write Depth Buffer", "bool", ps3.writeDepthBuffer.toString())
put("PS3/Video", "Read Color Buffers", "bool", ps3.readColorBuffers.toString())
@@ -1064,6 +1111,12 @@ data class Settings(
put("PS3/Net", "Internet enabled", "enum", ps3.netEnabled.toString())
put("PS3/Net", "PSN status", "enum", ps3.psnStatus.toString())
put("PS3/Net", "UPNP Enabled", "bool", ps3.upnpEnabled.toString())
put("PS3/System", "Enter button assignment", "enum", ps3.enterButtonAssign.toString())
put("PS3/System", "Language", "enum", ps3.consoleLanguage.toString())
put("PS3/System", "License Area", "enum", ps3.consoleRegion.toString())
put("PS3/System", "Keyboard Type", "enum", ps3.keyboardType.toString())
put("PS3/System", "Date Format", "enum", ps3.dateFormat.toString())
put("PS3/System", "Time Format", "enum", ps3.timeFormat.toString())
put("PS3/Core", "SPU XFloat Accuracy", "enum", ps3.spuXFloat.toString())
put("PS3/Core", "Accurate SPU Reservations", "bool", ps3.accurateSpuRsv.toString())
put("PS3/Core", "Accurate Cache Line Stores", "bool", ps3.accurateCacheLine.toString())
@@ -1228,12 +1281,10 @@ data class Settings(
put("MemoryCards", "Slot1_Filename", "string", memoryCardSlot1Filename.ifEmpty { "mcd001.ps2" })
put("MemoryCards", "Slot2_Enable", "bool", memoryCardSlot2Enabled.toString())
put("MemoryCards", "Slot2_Filename", "string", memoryCardSlot2Filename.ifEmpty { "mcd002.ps2" })
// USB keyboard (#254). Persist [USB1] Type so USBOptions::LoadSave attaches
// the emulated HID keyboard on the next boot (or ApplySettings). The live
// attach/detach on a running VM is done via NativeApp.usbSetKeyboardEnabled
// below (CheckForConfigChanges recreates the device), since a plain
// setSetting write doesn't reattach USB devices on its own.
put("USB1", "Type", "string", if (usbKeyboard) "hidkbd" else "None")
// Keyboard: NOT written here. [USB1] Type = hidkbd is a PCSX2 key -- there is
// no such USB device in RPCS3, so that write only ever reached
// Unsupported.note("USB1/Type"). The PS3 equivalent is the keyboard handler,
// pushed by NativeApp.usbSetKeyboardEnabled below.
// Recompiler enables. Picked up by VMManager::ApplySettings →
// SysCpuProviderPack rebind. Toggling these on a running VM swaps
// the dispatch pointer; existing JIT block caches are flushed by
@@ -1289,10 +1340,8 @@ data class Settings(
NativeApp.osdShowVersion(osdShowVersion)
NativeApp.osdShowSettings(osdShowSettings)
NativeApp.osdShowInputs(osdShowInputs)
// USB keyboard (#254): live attach/detach on the running VM. A plain
// setSetting("USB1","Type",...) write is persisted but doesn't reattach
// USB devices, so drive the device (re)creation explicitly. No-op before
// the VM exists — the persisted Type above handles the cold boot.
// Keyboard handler (#254). Installed by Emulator::Load, so this is a persist,
// not a live attach: a game already running keeps whatever it booted with.
NativeApp.usbSetKeyboardEnabled(0, usbKeyboard)
// Vblank at the PS3's own rate, pushed on every apply rather than left to a
// migration.
@@ -2008,6 +2057,9 @@ data class Settings(
put("ps3MsaaMode", ps3.msaaMode)
put("ps3AudioCubebBackend", ps3.audioCubebBackend)
put("ps3ShaderMode", ps3.shaderMode)
put("ps3FrameGeneration", ps3.frameGeneration)
put("ps3FrameGenPerformance", ps3.frameGenPerformance)
put("ps3FrameGenFlowScale", ps3.frameGenFlowScale)
put("ps3WriteColorBuffers", ps3.writeColorBuffers)
put("ps3GpuTurbo", ps3.gpuTurbo)
put("ps3SilenceAllLogs", ps3.silenceAllLogs)
@@ -2032,6 +2084,12 @@ data class Settings(
put("ps3NetEnabled", ps3.netEnabled)
put("ps3PsnStatus", ps3.psnStatus)
put("ps3UpnpEnabled", ps3.upnpEnabled)
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
put("ps3ConsoleLanguage", ps3.consoleLanguage)
put("ps3ConsoleRegion", ps3.consoleRegion)
put("ps3KeyboardType", ps3.keyboardType)
put("ps3DateFormat", ps3.dateFormat)
put("ps3TimeFormat", ps3.timeFormat)
put("ps3SpuXFloat", ps3.spuXFloat)
put("ps3AccurateSpuRsv", ps3.accurateSpuRsv)
put("ps3AccurateCacheLine", ps3.accurateCacheLine)
@@ -2346,6 +2404,9 @@ data class Settings(
msaaMode = json.optInt("ps3MsaaMode", def.ps3.msaaMode),
audioCubebBackend = json.optInt("ps3AudioCubebBackend", def.ps3.audioCubebBackend),
shaderMode = json.optInt("ps3ShaderMode", def.ps3.shaderMode),
frameGeneration = json.optInt("ps3FrameGeneration", def.ps3.frameGeneration),
frameGenPerformance = json.optBoolean("ps3FrameGenPerformance", def.ps3.frameGenPerformance),
frameGenFlowScale = json.optInt("ps3FrameGenFlowScale", def.ps3.frameGenFlowScale),
writeColorBuffers = json.optBoolean("ps3WriteColorBuffers", def.ps3.writeColorBuffers),
gpuTurbo = json.optBoolean("ps3GpuTurbo", def.ps3.gpuTurbo),
silenceAllLogs = json.optBoolean("ps3SilenceAllLogs", def.ps3.silenceAllLogs),
@@ -2370,6 +2431,12 @@ data class Settings(
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
consoleLanguage = json.optInt("ps3ConsoleLanguage", def.ps3.consoleLanguage),
consoleRegion = json.optInt("ps3ConsoleRegion", def.ps3.consoleRegion),
keyboardType = json.optInt("ps3KeyboardType", def.ps3.keyboardType),
dateFormat = json.optInt("ps3DateFormat", def.ps3.dateFormat),
timeFormat = json.optInt("ps3TimeFormat", def.ps3.timeFormat),
spuXFloat = json.optInt("ps3SpuXFloat", def.ps3.spuXFloat),
accurateSpuRsv = json.optBoolean("ps3AccurateSpuRsv", def.ps3.accurateSpuRsv),
accurateCacheLine = json.optBoolean("ps3AccurateCacheLine", def.ps3.accurateCacheLine),
@@ -2664,6 +2731,9 @@ data class Settings(
if (current.ps3.msaaMode != base.ps3.msaaMode) j.put("ps3MsaaMode", current.ps3.msaaMode)
if (current.ps3.audioCubebBackend != base.ps3.audioCubebBackend) j.put("ps3AudioCubebBackend", current.ps3.audioCubebBackend)
if (current.ps3.shaderMode != base.ps3.shaderMode) j.put("ps3ShaderMode", current.ps3.shaderMode)
if (current.ps3.frameGeneration != base.ps3.frameGeneration) j.put("ps3FrameGeneration", current.ps3.frameGeneration)
if (current.ps3.frameGenPerformance != base.ps3.frameGenPerformance) j.put("ps3FrameGenPerformance", current.ps3.frameGenPerformance)
if (current.ps3.frameGenFlowScale != base.ps3.frameGenFlowScale) j.put("ps3FrameGenFlowScale", current.ps3.frameGenFlowScale)
if (current.ps3.writeColorBuffers != base.ps3.writeColorBuffers) j.put("ps3WriteColorBuffers", current.ps3.writeColorBuffers)
if (current.ps3.gpuTurbo != base.ps3.gpuTurbo) j.put("ps3GpuTurbo", current.ps3.gpuTurbo)
if (current.ps3.silenceAllLogs != base.ps3.silenceAllLogs) j.put("ps3SilenceAllLogs", current.ps3.silenceAllLogs)
@@ -2688,6 +2758,12 @@ data class Settings(
if (current.ps3.netEnabled != base.ps3.netEnabled) j.put("ps3NetEnabled", current.ps3.netEnabled)
if (current.ps3.psnStatus != base.ps3.psnStatus) j.put("ps3PsnStatus", current.ps3.psnStatus)
if (current.ps3.upnpEnabled != base.ps3.upnpEnabled) j.put("ps3UpnpEnabled", current.ps3.upnpEnabled)
if (current.ps3.enterButtonAssign != base.ps3.enterButtonAssign) j.put("ps3EnterButtonAssign", current.ps3.enterButtonAssign)
if (current.ps3.consoleLanguage != base.ps3.consoleLanguage) j.put("ps3ConsoleLanguage", current.ps3.consoleLanguage)
if (current.ps3.consoleRegion != base.ps3.consoleRegion) j.put("ps3ConsoleRegion", current.ps3.consoleRegion)
if (current.ps3.keyboardType != base.ps3.keyboardType) j.put("ps3KeyboardType", current.ps3.keyboardType)
if (current.ps3.dateFormat != base.ps3.dateFormat) j.put("ps3DateFormat", current.ps3.dateFormat)
if (current.ps3.timeFormat != base.ps3.timeFormat) j.put("ps3TimeFormat", current.ps3.timeFormat)
if (current.ps3.spuXFloat != base.ps3.spuXFloat) j.put("ps3SpuXFloat", current.ps3.spuXFloat)
if (current.ps3.accurateSpuRsv != base.ps3.accurateSpuRsv) j.put("ps3AccurateSpuRsv", current.ps3.accurateSpuRsv)
if (current.ps3.accurateCacheLine != base.ps3.accurateCacheLine) j.put("ps3AccurateCacheLine", current.ps3.accurateCacheLine)
@@ -2963,6 +3039,9 @@ data class Settings(
msaaMode = if (overrides.has("ps3MsaaMode")) overrides.getInt("ps3MsaaMode") else base.ps3.msaaMode,
audioCubebBackend = if (overrides.has("ps3AudioCubebBackend")) overrides.getInt("ps3AudioCubebBackend") else base.ps3.audioCubebBackend,
shaderMode = if (overrides.has("ps3ShaderMode")) overrides.getInt("ps3ShaderMode") else base.ps3.shaderMode,
frameGeneration = if (overrides.has("ps3FrameGeneration")) overrides.getInt("ps3FrameGeneration") else base.ps3.frameGeneration,
frameGenPerformance = if (overrides.has("ps3FrameGenPerformance")) overrides.getBoolean("ps3FrameGenPerformance") else base.ps3.frameGenPerformance,
frameGenFlowScale = if (overrides.has("ps3FrameGenFlowScale")) overrides.getInt("ps3FrameGenFlowScale") else base.ps3.frameGenFlowScale,
writeColorBuffers = if (overrides.has("ps3WriteColorBuffers")) overrides.getBoolean("ps3WriteColorBuffers") else base.ps3.writeColorBuffers,
gpuTurbo = if (overrides.has("ps3GpuTurbo")) overrides.getBoolean("ps3GpuTurbo") else base.ps3.gpuTurbo,
silenceAllLogs = if (overrides.has("ps3SilenceAllLogs")) overrides.getBoolean("ps3SilenceAllLogs") else base.ps3.silenceAllLogs,
@@ -2987,6 +3066,12 @@ data class Settings(
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
enterButtonAssign = if (overrides.has("ps3EnterButtonAssign")) overrides.getInt("ps3EnterButtonAssign") else base.ps3.enterButtonAssign,
consoleLanguage = if (overrides.has("ps3ConsoleLanguage")) overrides.getInt("ps3ConsoleLanguage") else base.ps3.consoleLanguage,
consoleRegion = if (overrides.has("ps3ConsoleRegion")) overrides.getInt("ps3ConsoleRegion") else base.ps3.consoleRegion,
keyboardType = if (overrides.has("ps3KeyboardType")) overrides.getInt("ps3KeyboardType") else base.ps3.keyboardType,
dateFormat = if (overrides.has("ps3DateFormat")) overrides.getInt("ps3DateFormat") else base.ps3.dateFormat,
timeFormat = if (overrides.has("ps3TimeFormat")) overrides.getInt("ps3TimeFormat") else base.ps3.timeFormat,
spuXFloat = if (overrides.has("ps3SpuXFloat")) overrides.getInt("ps3SpuXFloat") else base.ps3.spuXFloat,
accurateSpuRsv = if (overrides.has("ps3AccurateSpuRsv")) overrides.getBoolean("ps3AccurateSpuRsv") else base.ps3.accurateSpuRsv,
accurateCacheLine = if (overrides.has("ps3AccurateCacheLine")) overrides.getBoolean("ps3AccurateCacheLine") else base.ps3.accurateCacheLine,

Some files were not shown because too many files have changed in this diff Show More