141 Commits
Author SHA1 Message Date
jpolo1224 a7093fe962 Release: 0.9.3 (versionCode 18) 2026-08-20 08:35:23 -04:00
jpolo1224 07a24cf71d PPU: stop on a compile out-of-memory rather than limping on
Continuing with the modules that did compile is correct -- the dispatcher
entry for an uncompiled function interprets, nothing runs garbage -- but it is
per-instruction dispatch and it is slower than the interpreter outright. Saint
Seiya measured 6fps against 23 with most of its modules missing.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Three things this deliberately does not do:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Above that, three dead links in a row.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

The STOCK entry is not optional. This port keeps one global config.yml, so a value
written for one game persists into the next; without the stock default, booting
Yakuza would leave FIFO reordering disabled for everything launched afterwards.
Same trap the Uncharted 3 entry documents.
2026-08-19 16:46:21 -04:00
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
FlexBy420 3c15df4e4d Update sceNpTrophy.cpp 2026-08-18 14:19:42 +02:00
kd-11 719cf8a54a gl: Fix build warning 2026-08-18 13:47:28 +03:00
kd-11 1b879360b8 rsx: Fix OOB section writes generated due to mipmap dimension clamping 2026-08-18 13:47:28 +03:00
kd-11 ad059d03af vk: Avoid redundant copy when writing to mip level or Z layer 2026-08-18 13:47:28 +03:00
kd-11 78ba581137 vk: Extend copy image API to support 3D offsets and extents 2026-08-18 13:47:28 +03:00
kd-11 cd044148be gl: Avoid redundant copies when copying to mipmaps or 3D slices 2026-08-18 13:47:28 +03:00
kd-11 91cd82a0c2 gl: Extend copy image API to allow explicit 3D offsets 2026-08-18 13:47:28 +03:00
kd-11 e7c3d6ab26 gl: Implement support for explicit multi-layer image copy operations 2026-08-18 13:47:28 +03:00
kd-11 a88331bb22 rsx/vk: Implement support for multi-layer, multi-level and explicit mip/layer image transfers 2026-08-18 13:47:28 +03:00
kd-11 6892ee4c2e rsx: Fix mipmap gather source offsets 2026-08-18 13:47:28 +03:00
Megamouse 0059e4e92e unpkg: fix OOB read at end of file
Use sizeof(u128) instead of 16.
Clear padding after archive_read_block.
Use aligned_div instead of manually aligning blocks.
Fix local_buf size when using raw ptr of the original buffer.
2026-08-18 10:20:30 +02:00
Megamouse 46b7428fad unpkg: also check potential overflow in pkg data size check 2026-08-18 10:20:30 +02:00
Megamouse 582b5ba29e unpkg: fix OOB memset, use safe versions of write_to_ptr and read_from_ptr 2026-08-18 10:20:30 +02:00
FlexBy420 cb175278b6 RPCN: Sync trophies (#18760)
Add synching local trophies with RPCN server trophies, allows users to
essentially cloud save their trophies when they are connected to RPCN.

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

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

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

Co-authored-by: jpolo1224 <jpolo1224@gmail.com>
2026-08-18 03:12:13 +02:00
Megamouse de33fda28c rsx_debugger: fix g8b8 conversion 2026-08-17 22:42:05 +02:00
Neil Monday f9f88aa9e5 Use max() to bring negative floats up to 0.0 before uint conversion. 2026-08-17 15:17:17 +02:00
Ani 3be5aa99cc gui: Correctly disable the View Folder button 2026-08-17 02:22:11 +02:00
Zion Nimchuk 3e68a7f385 Add 60 second retry to translation downloads in CI to avoid rate limits 2026-08-17 01:20:35 +02:00
kd-11 9a4b849260 rsx: Fix fbo offset scaling when src and dst bpp is mismatched 2026-08-17 00:15:54 +03:00
kd-11 9a24c8d11f gl/vk: Fix flattened interpreter input subresource range computation 2026-08-17 00:15:54 +03:00
kd-11 0617eff348 rsx: Ensure ref_address is properly set for all sampled images 2026-08-17 00:15:54 +03:00
kd-11 003b368980 rsx: Drill down the copy specification from descriptors when handling dynamic copies 2026-08-17 00:15:54 +03:00
kd-11 6f5f198acd rsx: Fix check for cyclic ref in fast_fbo_check 2026-08-17 00:15:54 +03:00
kd-11 107b751a43 rsx: Enable fast path when scanning for 3D mipmaps 2026-08-17 00:15:54 +03:00
kd-11 4475671bbf RSX: Allow process_framebuffer_resource_fast to take in descriptors with an offset
- Allows to skip going through the merge route when we already have a good match
2026-08-17 00:15:54 +03:00
kd-11 7e35f59997 RSX: Implement host-side mipmap scanning for 3D textures 2026-08-17 00:15:54 +03:00
kd-11 970d745818 rsx: Respect mip levels actually used during image reconstruction 2026-08-17 00:15:54 +03:00
kd-11 adec3ae7f9 rsx: Implement per-mip-level size calculation logic and use it to properly compute 3D texture slice height 2026-08-17 00:15:54 +03:00
kd-11 5e2d0eb762 rsx: Fix incorrect calculation of texture size when border texels are present
- The computation did not match get_subresources_layout behavior.
2026-08-17 00:15:54 +03:00
kd-11 26782525f4 rsx: Fix get_texture_size for 3D textures with mipmaps
- Depth also shrinks for every mip level
2026-08-17 00:15:54 +03:00
kd-11 d0e6d4eefc rsx: Minor improvements to texture cache
- Adds depth to temp subresource key to avoid 3D mismatch (resample)
- Fix narrowing warning for block_h calculations
2026-08-17 00:15:54 +03:00
kd-11 f19d398bec rsx: Check for completeness in X when gathering slices
- This was intentionally ommitted as a speedhack before, but makes sense to check just in case.
2026-08-17 00:15:54 +03:00
kd-11 6b80ac4805 rsx: Simplify merged source sorting when selecting slices
- Sort ranges is a relic and the information is duplicated in the sort_list object
2026-08-17 00:15:54 +03:00
kd-11 13ceef9f46 rsx: Fix slice gather from local resource (e.g blit engine output) 2026-08-17 00:15:54 +03:00
Walter ffc50905a6 [SPU LLVM] Avoid GFNI combine bug in SHUFB
Due to a bug where `SHUFB`'s GFNI constant generation path expects to be combine using a select instead of a OR, it was causing issues with on non-AVX512 CPUs so support was reverted (see #19217). That can still happen on AVX512 CPUs when the shuffle is single source. This patch fixes it and re-lower the target requirements back to just GFNI by avoiding the OR well on the GFNI path. I also renamed a variable and added a comment to better clarify its behavior.
2026-08-16 19:35:42 +03:00
kd-11 cbc7b60ba5 rsx: Cleanup 2026-08-16 13:50:03 +03:00
kd-11 47efd770a8 rsx: Clean up get_merged_texture_memory_region
- Cleaner generation of src_area and dst_area outputs.
- Normalized comparisons in 1bpp space then convert to target space after.
2026-08-16 13:50:03 +03:00
kd-11 f53547b173 rsx: Refactor deferred_subresource constructor into discrete wrappers for each output intent
- Instead of filling over 10 arguments and having the ctor silently drop half of them, we create proper wrappers to construct objects for a singular purpose.
2026-08-16 13:50:03 +03:00
kd-11 cb5b866c61 rsx: Refactor deferred_subresource to be more explicit
- Use defined src and dst rects as well as the transformation if any to be applied.
2026-08-16 13:50:03 +03:00
Zion Nimchuk 2f3c0f04d2 Update docker with updated SDL3 2026-08-16 09:57:33 +02:00
schm1dtmac 3f493fb209 [Qt] Hide titlebars by default 2026-08-16 02:07:48 +02:00
Antonino Di Guardo f7eb0d8d76 Enrich game list title (#19229)
Add on Game List title a brief recap of total number of entries in the
list and total number of Disc, HDD and all the other remaining types of
content.
2026-08-15 23:19:01 +00:00
digant73 fc93d932c8 Swap PR number with PR text in update manager 2026-08-15 13:28:58 +02:00
digant73 3cbf9b8b6c fix crash with vfs exception 2026-08-15 04:39:42 +03:00
kd-11 12b1efc266 rsx/fp: Fix decoding of LOOP and REP instructions
- Verified with hardware tests. RSX does not support proper loops.
- The LOOP/REP instruction simply codes a "REPEAT n" instruction for a block of code.
- There is no accumulator register. The compiler emits a preamble inside the loop block to simulate the running counter.
- Oddly enough, the original start and step values are stored in the instruction but are unused. Maybe useful for debugging real hardware?
2026-08-14 11:15:20 +03:00
Megamouse 4c63acfb40 Qt: Add unofficial build warning 2026-08-14 02:07:18 +02:00
Megamouse 3f4364fe74 Qt: Decrease layout margin in settings_dialog 2026-08-14 01:02:56 +02:00
Ani ee43ab7362 Revert "[SPU LLVM] Decrease SHUFB's constant generation target requirements"
This reverts commit 26e37d8c8c.
2026-08-14 00:19:17 +02:00
Megamouse 2dc6cf014c Qt: add Open Custom Gamepad Config Folder action to game list context menu 2026-08-13 21:36:51 +02:00
Megamouse c285c2fb41 Qt: fix settings_dialog tab index 2026-08-13 19:14:26 +02:00
Megamouse c41595e79f Qt: implement auto_scroll_label and use it for the settings descriptions 2026-08-13 14:42:31 +02:00
Lalit Shankar Chowdhury bf541b5828 qt: make settings description static 2026-08-13 14:42:31 +02:00
Megamouse 41e2d101e7 Update discord-rpc 2026-08-13 11:55:39 +02:00
kd-11 c27b38f300 vk: Run GC on the driver manager thread 2026-08-13 11:39:34 +03:00
kd-11 5bd7fd7817 vk: Implement an asynchronous driver manager thread 2026-08-13 11:39:34 +03:00
kd-11 7f9b8d23cd vk: Enhanced thread safety when handling the query subpool allocation cache 2026-08-13 11:39:34 +03:00
kd-11 01e3fed466 vk: Enhanced thread safety when handling descriptor subpools 2026-08-13 11:39:34 +03:00
kd-11 ea0e10e704 vk: Ensure all drawable surfaces invalidate fbo cache on deletion 2026-08-13 11:39:34 +03:00
kd-11 c2eb11eae6 vk: Seal some data leaks with the framebuffer cache
- Maybe fixes some device lost crashes when running in VRAM-constrained situations
2026-08-13 11:39:34 +03:00
Walter 26e37d8c8c [SPU LLVM] Decrease SHUFB's constant generation target requirements
The constant generation AVX512-ICL path only requires the feature GFNI, which exists on non-AVX512 CPUs. This was a hold-over from when the shuffle step was merged together. (The proceeding unsigned minimum is from SSE2)
2026-08-12 15:38:43 +03:00
Lalit Shankar Chowdhury 9b1eb45a47 PPU: implement AVX2 path for gv_rol32 2026-08-12 14:01:33 +03:00
kd-11 92870a3d4e rsx: nv0039 cleanup
- Enforce some behavior observed on real hardware
2026-08-12 03:33:10 +03:00
Nick Gregory f7cfdc6570 rsx: Fix nv0039 image (de)interleaving functionality 2026-08-11 19:40:15 +00:00
Megamouse a603fbba8c Update discord-rpc 2026-08-11 15:52:57 +02:00
Megamouse db907a2586 TAR: Simplify result string creation 2026-08-11 10:55:02 +02:00
Megamouse b9cee7a3a9 Fix IsPathInsideDir arguments during file extraction 2026-08-11 10:55:02 +02:00
Megamouse d7c15851b4 Qt: run initial dialogs on the main event loop 2026-08-11 08:56:42 +02:00
Megamouse a723152dea Qt: make sure progress dialog stays hidden in the beginning 2026-08-11 08:56:42 +02:00
RipleyTom 2f4034590f sys_net: fix possible event gap in recvfrom 2026-08-10 20:03:56 +03:00
RipleyTom 7b6a8cc2d6 sys_net: more fixes
Add parameter checks to sys_net_infoctl
Fix P2P getpeername() fatal
Fix possible deadlock in tcp_timeout_monitor
Remove vport assert from poll
Add upgrade path for new P2PS state in savestates
Fix P2PS close_stream() not waking threads
Fix possible deadlock in P2PS connect()
Ensure RST is sent on unhandled packets
2026-08-10 20:03:56 +03:00
RipleyTom 6df35891ad sys_net: fixes and improvements
Add thread lock for p2p sockets(used for poll/select to avoid event gap)
Fix poll/select returning EINTR if no sockets were polled
Implement sys_net_infoctl cmd 6(sys_net_get_sockinfo)
Cleanup sys_net_infoctl cmd 9 code
Add SYS_NET_STATE_* values to header
Rewrite P2P and P2PS poll/select implementations
Add extra P2PS state for disconnected and report it as readable with 0 bytes(EOF)
Fix P2PS sockets connecting without being bound first missing hashmap insert
Add missing error check in sceNpSignalingActivateConnection
2026-08-10 20:03:56 +03:00
Megamouse eaef23d43a unself: fix overflow checks 2026-08-10 17:35:26 +02:00
Megamouse 8cde4b2153 unself: fix more potential OOB 2026-08-10 17:35:26 +02:00
Megamouse ed3af84437 unself: add some sanity checks and optimize uncompress a bit 2026-08-10 17:35:26 +02:00
Megamouse 81b85e55e4 unself: cache buffer during decrypt 2026-08-10 17:35:26 +02:00
Megamouse 9af11f5cf1 unself: add some OOB checks 2026-08-10 17:35:26 +02:00
Megamouse d6d5c60823 ISO: mark archive as invalid on error and add sanity checks 2026-08-10 14:45:00 +02:00
Megamouse 0230580a88 ISO: exit loop at end of file 2026-08-10 14:45:00 +02:00
Megamouse ae98583ed2 ISO: ensure filename size during parsing 2026-08-10 14:45:00 +02:00
Megamouse 628ea5ec15 ISO: ensure we're not trying to install files outside of their parent 2026-08-10 14:45:00 +02:00
Megamouse 400d9a1c24 ISO: fix potential overflow 2026-08-10 14:45:00 +02:00
Megamouse 58ef670992 Add file path validations during extraction 2026-08-10 12:35:49 +02:00
Megamouse de13ae4753 Initialize emu callbacks before initializing the emulator 2026-08-10 12:35:49 +02:00
Megamouse f48ca59235 unedat: fix division by 0 2026-08-10 10:53:30 +02:00
Megamouse 804e06356b unedat: fix some data types to prevent overflow 2026-08-10 10:53:30 +02:00
Megamouse 70e3e15e2f unedat: Fix more potential OOB reads 2026-08-10 10:53:30 +02:00
Megamouse 852407b8cb unedat: don't use raw pointers everywhere and use more const 2026-08-10 10:53:30 +02:00
Megamouse f945ab62c0 Fix steam shortcut creation
Look for AutoLogin by first.
Look for MostRecent and Timestamp as fallbacks.
2026-08-10 09:10:42 +02:00
Megamouse ec208289fa elf: remove unnecessary -1 on both sides of a size check to prevent underflow 2026-08-10 02:34:41 +02:00
Megamouse b93c4733a8 Fix OOB buffer read in TRPLoader::LoadHeader 2026-08-10 01:33:53 +02:00
Walter 6d42df0ffc CPUTranslator: Add missing Intel Intrinsic header
MSVC compiles without it, but other compilers need it to be explicitly included.
2026-08-09 09:13:41 +03:00
Walter 502ea1f436 CPUTranslator: Additional constant folding for intrinsics
LLVM is unable to constant fold its X86 intrinsics directly, so this patch adds manual evaluation by calling their equivalent Intel Intrinsic.
2026-08-09 09:13:41 +03:00
Lalit Shankar Chowdhury 8d034a36e8 vk: Sort enumerated GPUs according to priority 2026-08-08 18:39:16 +03:00
Lalit Shankar Chowdhury 75fea2216b Qt: Remember last used path when adding games from folder or ISO
Signed-off-by: Lalit Shankar Chowdhury <lalitshankarch@gmail.com>
2026-08-08 16:29:09 +02:00
Florin9doi 3d587726a2 spu: Clear the MFC_LSA_offs bits higher than the limit 2026-08-05 15:19:12 +03:00
Sanjay Govind f3f52feddf Update SDL to 3.4.14 2026-08-05 08:47:26 +02:00
174 changed files with 13144 additions and 6982 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ if [ "$DEPLOY_APPIMAGE" = "true" ]; then
# Download translations
mkdir -p "./AppDir/usr/translations"
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -45,7 +45,7 @@ if [ "$DEPLOY_APPIMAGE" = "true" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+1 -1
View File
@@ -29,7 +29,7 @@ rm -rf "rpcs3.app/Contents/Frameworks/QtPdf.framework" \
mkdir -p "rpcs3.app/Contents/translations"
ZIP_URL="https://github.com/RPCS3/rpcs3_translations/releases/latest/download/RPCS3-languages.zip"
echo "Downloading translations from: $ZIP_URL"
if curl -fsSL "$ZIP_URL" -o "translations.zip"; then
if curl -fsSL --retry 3 --retry-delay 60 "$ZIP_URL" -o "translations.zip"; then
echo "Successfully downloaded translations."
if unzip -o translations.zip -d "rpcs3.app/Contents/translations" >/dev/null 2>&1; then
rm -f translations.zip
+2 -2
View File
@@ -28,7 +28,7 @@ curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -f ISO-8859-1 -t UTF-8
# Download translations
mkdir -p ./bin/share/qt6/translations
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -36,7 +36,7 @@ if [ -z "$ZIP_URL" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+2 -2
View File
@@ -18,7 +18,7 @@ curl -fsSL 'https://api.rpcs3.net/config/?api=v1' | iconv -t UTF-8 1> ./bin/GuiC
# Download translations
mkdir -p ./bin/qt6/translations
ZIP_URL=$(curl -fsSL "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
ZIP_URL=$(curl -fsSL --retry 3 --retry-delay 60 "https://api.github.com/repos/RPCS3/rpcs3_translations/releases/latest" \
| grep "browser_download_url" \
| grep "RPCS3-languages.zip" \
| cut -d '"' -f 4)
@@ -26,7 +26,7 @@ if [ -z "$ZIP_URL" ]; then
echo "Failed to find RPCS3-languages.zip in the latest release. Continuing without translations."
else
echo "Downloading translations from: $ZIP_URL"
curl -L -o translations.zip "$ZIP_URL" || {
curl -fsSL --retry 3 --retry-delay 60 -o translations.zip "$ZIP_URL" || {
echo "Failed to download translations.zip. Continuing without translations."
exit 0
}
+4 -4
View File
@@ -30,23 +30,23 @@ jobs:
matrix:
include:
- os: ubuntu-24.04
docker_img: "rpcs3/rpcs3-ci-jammy:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy:2.1"
build_sh: "/rpcs3/.ci/build-linux.sh"
compiler: clang
UPLOAD_COMMIT_HASH: d812f1254a1157c80fd402f94446310560f54e5f
UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux"
- os: ubuntu-24.04
docker_img: "rpcs3/rpcs3-ci-jammy:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy:2.1"
build_sh: "/rpcs3/.ci/build-linux.sh"
compiler: gcc
- os: ubuntu-24.04-arm
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.1"
build_sh: "/rpcs3/.ci/build-linux-aarch64.sh"
compiler: clang
UPLOAD_COMMIT_HASH: a1d35836e8d45bfc6f63c26f0a3e5d46ef622fe1
UPLOAD_REPO_FULL_NAME: "rpcs3/rpcs3-binaries-linux-arm64"
- os: ubuntu-24.04-arm
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.0"
docker_img: "rpcs3/rpcs3-ci-jammy-aarch64:2.1"
build_sh: "/rpcs3/.ci/build-linux-aarch64.sh"
compiler: gcc
name: RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }}
+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
+14 -2
View File
@@ -332,7 +332,15 @@ struct MemoryManager1 : llvm::RTDyldMemoryManager
{
const u64 pagea = utils::align(oldp, page_quarter);
const u64 psize = utils::align(std::min(newp, c_page_size) - pagea, page_quarter);
utils::memory_commit(reinterpret_cast<u8*>(block) + (pagea % c_max_size), psize, prot);
// try_ rather than memory_commit: a commit failure here is the device being out of
// memory, and the caller has a real fallback -- the module does not compile and its
// functions are interpreted. The fatal version reported it as "LLVM crash recovery
// invoked", which reads like a codegen bug and sent this diagnosis the wrong way.
if (!utils::try_memory_commit(reinterpret_cast<u8*>(block) + (pagea % c_max_size), psize, prot))
{
fmt::throw_exception("Out of memory (commit failed: size=0x%x, align=0x%x)", size, align);
}
// Advance
oldp = pagea + psize;
@@ -343,7 +351,11 @@ struct MemoryManager1 : llvm::RTDyldMemoryManager
// Allocate pages on demand
const u64 pagea = utils::align(oldp, c_page_size);
const u64 psize = utils::align(newp - pagea, c_page_size);
utils::memory_commit(reinterpret_cast<u8*>(block) + (pagea % c_max_size), psize, prot);
if (!utils::try_memory_commit(reinterpret_cast<u8*>(block) + (pagea % c_max_size), psize, prot))
{
fmt::throw_exception("Out of memory (commit failed: size=0x%x, align=0x%x)", size, align);
}
}
return reinterpret_cast<u8*>(block) + (olda % c_max_size);
+28
View File
@@ -24,6 +24,11 @@
#include <stacktrace>
#endif
// Not only under _WIN32 below: the access-violation handler prints a host backtrace on every
// platform, and on Android that is the only stack anyone gets -- the handler freezes the
// emulator rather than aborting, so no tombstone is ever written.
#include "stack_trace.h"
#ifdef _WIN32
#include <Windows.h>
#include <Psapi.h>
@@ -2317,6 +2322,29 @@ bool handle_access_violation(u32 addr, bool is_writing, bool is_exec, ucontext_t
{
vm_log.notice("\n%s", dump_useful_thread_info());
vm_log.fatal("Access violation %s location 0x%x (%s)", is_writing ? "writing" : (is_exec ? "executing" : "reading"), addr, (is_writing && vm::check_addr(addr)) ? "read-only memory" : "unmapped memory");
// The host stack, which is the half that was missing.
//
// dump_useful_thread_info prints GUEST state, and for a fault taken on an emulator
// thread rather than inside guest code that says where the emulator was in the game,
// not which of our functions dereferenced null. Nor is there a tombstone to fall back
// on: this path freezes the emulator instead of aborting, so the process survives and
// Android never writes one.
//
// Yakuza Dead Souls reads location 0xc on the RSX thread with the FIFO empty and
// parked at a self-jump -- so the fault is in whatever runs while no commands are
// pending, and there are several candidates. Naming the frame settles it.
if (const auto stack = utils::get_backtrace_symbols(utils::get_backtrace(32)); !stack.empty())
{
std::string out;
for (usz i = 0; i < stack.size(); i++)
{
fmt::append(out, "\n #%02u %s", i, stack[i]);
}
vm_log.fatal("Host backtrace:%s", out);
}
}
while (Emu.IsPausedOrReady())
+57 -3
View File
@@ -724,15 +724,24 @@ struct coord3_base
struct { T width, height, depth; };
};
constexpr coord3_base() : position{}, size{}
constexpr coord3_base()
: position{}, size{}
{
}
constexpr coord3_base(const position3_base<T>& position, const size3_base<T>& size) : position{ position }, size{ size }
constexpr coord3_base(const position3_base<T>& position, const size3_base<T>& size)
: position{ position }, size{ size }
{
}
constexpr coord3_base(T x, T y, T z, T width, T height, T depth) : x{ x }, y{ y }, z{ z }, width{ width }, height{ height }, depth{ depth }
constexpr coord3_base(T x, T y, T z, T width, T height, T depth)
: x{ x }, y{ y }, z{ z }, width{ width }, height{ height }, depth{ depth }
{
}
constexpr coord3_base(const area_base<T>& area, T z = 0, T depth = 1)
: x{ area.x1 }, y{ area.y1 }, z{ z }
, width{ area.x2 - area.x1 }, height{ area.y2 - area.y1 }, depth{ depth }
{
}
@@ -755,6 +764,51 @@ struct coord3_base
{
return{ static_cast<NT>(x), static_cast<NT>(y), static_cast<NT>(z), static_cast<NT>(width), static_cast<NT>(height), static_cast<NT>(depth) };
}
void flip_horizontal()
requires std::is_signed_v<T>
{
auto x2 = x + width;
x = x2;
width = -width;
}
void flip_vertical()
requires std::is_signed_v<T>
{
auto y2 = y + height;
y = y2;
height = -height;
}
bool is_flipped() const
requires std::is_signed_v<T>
{
return width < 0 || height < 0 || depth < 0;
}
area_base<T> to_area() const
{
return { x, y, x + width, y + height };
}
T abs_width() const
requires std::is_signed_v<T>
{
return width < 0 ? -width : width;
}
T abs_height() const
requires std::is_signed_v<T>
{
return height < 0 ? -height : height;
}
T abs_depth() const
requires std::is_signed_v<T>
{
return depth < 0 ? -depth : depth;
}
};
+259 -172
View File
@@ -1,172 +1,259 @@
#include "stdafx.h"
#include "stack_trace.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#define DBGHELP_TRANSLATE_TCHAR
#include <DbgHelp.h>
#include <codecvt>
#else
#include <execinfo.h>
#endif
namespace utils
{
#ifdef _WIN32
std::string wstr_to_utf8(LPWSTR data, int str_len)
{
if (!str_len)
{
return {};
}
// Calculate size
const auto length = WideCharToMultiByte(CP_UTF8, 0, data, str_len, NULL, 0, NULL, NULL);
// Convert
std::vector<char> out(length + 1, 0);
WideCharToMultiByte(CP_UTF8, 0, data, str_len, out.data(), length, NULL, NULL);
return out.data();
}
std::vector<void*> get_backtrace(int max_depth, PCONTEXT ctx)
{
static struct sym_initer_t
{
sym_initer_t() noexcept
{
SymInitialize(GetCurrentProcess(), NULL, TRUE);
}
~sym_initer_t() noexcept
{
SymCleanup(GetCurrentProcess());
}
} s_initer{};
std::vector<void*> result = {};
const auto hProcess = ::GetCurrentProcess();
const auto hThread = ::GetCurrentThread();
CONTEXT context{};
if (ctx)
context = *ctx;
else
RtlCaptureContext(&context);
STACKFRAME64 stack = {};
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrStack.Mode = AddrModeFlat;
stack.AddrFrame.Mode = AddrModeFlat;
#if defined(ARCH_X64)
const DWORD machineType = IMAGE_FILE_MACHINE_AMD64;
stack.AddrPC.Offset = context.Rip;
stack.AddrStack.Offset = context.Rsp;
stack.AddrFrame.Offset = context.Rbp;
#elif defined(ARCH_ARM64)
const DWORD machineType = IMAGE_FILE_MACHINE_ARM64;
stack.AddrPC.Offset = context.Pc;
stack.AddrStack.Offset = context.Sp;
stack.AddrFrame.Offset = context.Fp;
#else
#error "Unsupported architecture"
#endif
while (max_depth--)
{
if (!StackWalk64(
machineType,
hProcess,
hThread,
&stack,
&context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL))
{
break;
}
result.push_back(reinterpret_cast<void*>(stack.AddrPC.Offset));
}
return result;
}
std::vector<std::string> get_backtrace_symbols(const std::vector<void*>& stack)
{
std::vector<std::string> result = {};
std::vector<u8> symbol_buf(sizeof(SYMBOL_INFOW) + sizeof(TCHAR) * 256);
const auto hProcess = ::GetCurrentProcess();
auto sym = reinterpret_cast<SYMBOL_INFOW*>(symbol_buf.data());
sym->SizeOfStruct = sizeof(SYMBOL_INFOW);
sym->MaxNameLen = 256;
IMAGEHLP_LINEW64 line_info{};
line_info.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
SymInitialize(hProcess, NULL, TRUE);
SymSetOptions(SYMOPT_LOAD_LINES);
for (const auto& pointer : stack)
{
DWORD64 unused;
SymFromAddrW(hProcess, reinterpret_cast<DWORD64>(pointer), &unused, sym);
if (sym->NameLen)
{
std::string function_name = wstr_to_utf8(sym->Name, static_cast<int>(sym->NameLen));
// Attempt to get file and line information if available
DWORD unused2;
if (SymGetLineFromAddrW64(hProcess, reinterpret_cast<DWORD64>(pointer), &unused2, &line_info))
{
std::string full_path = fmt::format("%s:%u %s", wstr_to_utf8(line_info.FileName, -1), line_info.LineNumber, function_name);
result.push_back(std::move(full_path));
}
else
{
result.push_back(std::move(function_name));
}
}
else
{
result.push_back(fmt::format("rpcs3@0x%p", pointer));
}
}
return result;
}
#else
std::vector<void*> get_backtrace(int max_depth)
{
std::vector<void*> result(max_depth);
#ifndef ANDROID
int depth = backtrace(result.data(), max_depth);
result.resize(depth);
#endif
return result;
}
std::vector<std::string> get_backtrace_symbols(const std::vector<void*>& stack)
{
std::vector<std::string> result;
#ifndef ANDROID
result.reserve(stack.size());
const auto symbols = backtrace_symbols(stack.data(), static_cast<int>(stack.size()));
for (usz i = 0; i < stack.size(); ++i)
{
result.push_back(symbols[i]);
}
free(symbols);
#endif
return result;
}
#endif
}
#include "stdafx.h"
#include "stack_trace.h"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#define DBGHELP_TRANSLATE_TCHAR
#include <DbgHelp.h>
#include <codecvt>
#elif defined(ANDROID)
// bionic has no backtrace()/backtrace_symbols(), which is why both were compiled out here and
// every native crash on this port had to be read out of a tombstone or symbolized by hand.
// _Unwind_Backtrace is always present, and dladdr gives the library-relative offset that
// llvm-symbolizer wants.
#include <unwind.h>
#include <dlfcn.h>
#else
#include <execinfo.h>
#endif
namespace utils
{
#ifdef _WIN32
std::string wstr_to_utf8(LPWSTR data, int str_len)
{
if (!str_len)
{
return {};
}
// Calculate size
const auto length = WideCharToMultiByte(CP_UTF8, 0, data, str_len, NULL, 0, NULL, NULL);
// Convert
std::vector<char> out(length + 1, 0);
WideCharToMultiByte(CP_UTF8, 0, data, str_len, out.data(), length, NULL, NULL);
return out.data();
}
std::vector<void*> get_backtrace(int max_depth, PCONTEXT ctx)
{
static struct sym_initer_t
{
sym_initer_t() noexcept
{
SymInitialize(GetCurrentProcess(), NULL, TRUE);
}
~sym_initer_t() noexcept
{
SymCleanup(GetCurrentProcess());
}
} s_initer{};
std::vector<void*> result = {};
const auto hProcess = ::GetCurrentProcess();
const auto hThread = ::GetCurrentThread();
CONTEXT context{};
if (ctx)
context = *ctx;
else
RtlCaptureContext(&context);
STACKFRAME64 stack = {};
stack.AddrPC.Mode = AddrModeFlat;
stack.AddrStack.Mode = AddrModeFlat;
stack.AddrFrame.Mode = AddrModeFlat;
#if defined(ARCH_X64)
const DWORD machineType = IMAGE_FILE_MACHINE_AMD64;
stack.AddrPC.Offset = context.Rip;
stack.AddrStack.Offset = context.Rsp;
stack.AddrFrame.Offset = context.Rbp;
#elif defined(ARCH_ARM64)
const DWORD machineType = IMAGE_FILE_MACHINE_ARM64;
stack.AddrPC.Offset = context.Pc;
stack.AddrStack.Offset = context.Sp;
stack.AddrFrame.Offset = context.Fp;
#else
#error "Unsupported architecture"
#endif
while (max_depth--)
{
if (!StackWalk64(
machineType,
hProcess,
hThread,
&stack,
&context,
NULL,
SymFunctionTableAccess64,
SymGetModuleBase64,
NULL))
{
break;
}
result.push_back(reinterpret_cast<void*>(stack.AddrPC.Offset));
}
return result;
}
std::vector<std::string> get_backtrace_symbols(const std::vector<void*>& stack)
{
std::vector<std::string> result = {};
std::vector<u8> symbol_buf(sizeof(SYMBOL_INFOW) + sizeof(TCHAR) * 256);
const auto hProcess = ::GetCurrentProcess();
auto sym = reinterpret_cast<SYMBOL_INFOW*>(symbol_buf.data());
sym->SizeOfStruct = sizeof(SYMBOL_INFOW);
sym->MaxNameLen = 256;
IMAGEHLP_LINEW64 line_info{};
line_info.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
SymInitialize(hProcess, NULL, TRUE);
SymSetOptions(SYMOPT_LOAD_LINES);
for (const auto& pointer : stack)
{
DWORD64 unused;
SymFromAddrW(hProcess, reinterpret_cast<DWORD64>(pointer), &unused, sym);
if (sym->NameLen)
{
std::string function_name = wstr_to_utf8(sym->Name, static_cast<int>(sym->NameLen));
// Attempt to get file and line information if available
DWORD unused2;
if (SymGetLineFromAddrW64(hProcess, reinterpret_cast<DWORD64>(pointer), &unused2, &line_info))
{
std::string full_path = fmt::format("%s:%u %s", wstr_to_utf8(line_info.FileName, -1), line_info.LineNumber, function_name);
result.push_back(std::move(full_path));
}
else
{
result.push_back(std::move(function_name));
}
}
else
{
result.push_back(fmt::format("rpcs3@0x%p", pointer));
}
}
return result;
}
#elif defined(ANDROID)
namespace
{
struct unwind_state
{
void** current;
void** end;
};
_Unwind_Reason_Code unwind_collect(_Unwind_Context* ctx, void* arg)
{
auto* state = static_cast<unwind_state*>(arg);
// A frame with no PC is the end of what the unwinder can see; keep the frames
// gathered so far rather than discarding a partial stack, which is still the
// answer most of the time.
const auto pc = _Unwind_GetIP(ctx);
if (!pc)
{
return _URC_END_OF_STACK;
}
if (state->current == state->end)
{
return _URC_END_OF_STACK;
}
*state->current++ = reinterpret_cast<void*>(pc);
return _URC_NO_REASON;
}
}
std::vector<void*> get_backtrace(int max_depth)
{
std::vector<void*> result(max_depth);
unwind_state state{ result.data(), result.data() + max_depth };
_Unwind_Backtrace(&unwind_collect, &state);
result.resize(state.current - result.data());
return result;
}
std::vector<std::string> get_backtrace_symbols(const std::vector<void*>& stack)
{
std::vector<std::string> result;
result.reserve(stack.size());
for (void* const pointer : stack)
{
Dl_info info{};
if (!dladdr(pointer, &info) || !info.dli_fname)
{
result.push_back(fmt::format("0x%p", pointer));
continue;
}
// Library-relative, because that is what symbolizes. The shipped .so is stripped
// and loaded at a random base, so an absolute PC is useless on its own; this
// offset is what llvm-symbolizer takes against the unstripped build output.
const auto base = reinterpret_cast<uptr>(info.dli_fbase);
const auto off = reinterpret_cast<uptr>(pointer) - base;
// Basename only: the full path is the app's private data dir and the same for
// every frame.
std::string_view lib = info.dli_fname;
if (const auto slash = lib.find_last_of('/'); slash != umax)
{
lib.remove_prefix(slash + 1);
}
if (info.dli_sname)
{
result.push_back(fmt::format("%s+0x%x (%s)", lib, off, info.dli_sname));
}
else
{
result.push_back(fmt::format("%s+0x%x", lib, off));
}
}
return result;
}
#else
std::vector<void*> get_backtrace(int max_depth)
{
std::vector<void*> result(max_depth);
int depth = backtrace(result.data(), max_depth);
result.resize(depth);
return result;
}
std::vector<std::string> get_backtrace_symbols(const std::vector<void*>& stack)
{
std::vector<std::string> result;
result.reserve(stack.size());
const auto symbols = backtrace_symbols(stack.data(), static_cast<int>(stack.size()));
for (usz i = 0; i < stack.size(); ++i)
{
result.push_back(symbols[i]);
}
free(symbols);
return result;
}
#endif
}
+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
+2 -2
View File
@@ -32,8 +32,8 @@ android {
// 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 = 15
versionName = "0.9"
versionCode = 18
versionName = "0.9.3"
// 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.
@@ -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);
@@ -61,6 +62,21 @@ struct RPCSXApi {
int (*frameGenImportShaders)(std::string_view path);
int (*frameGenShaderCount)();
const char *(*frameGenShaderError)();
const char *(*rpcnGetConfig)();
void (*rpcnSetConfig)(std::string_view host, std::string_view npid,
std::string_view password, std::string_view token);
const char *(*rpcnCreateAccount)(std::string_view npid, std::string_view password,
std::string_view onlineName, std::string_view email);
const char *(*rpcnResendToken)(std::string_view npid, std::string_view password);
const char *(*rpcnSendResetToken)(std::string_view npid, std::string_view email);
const char *(*rpcnResetPassword)(std::string_view npid, std::string_view token,
std::string_view password);
const char *(*rpcnTestLogin)();
const char *(*rpcnAddHost)(std::string_view desc, std::string_view host);
const char *(*rpcnDelHost)(std::string_view desc, std::string_view host);
void (*rpcnResetHosts)();
void (*rpcnSetIpv6)(bool enabled);
const char *(*rpcnStatus)();
void (*settingsBeginBatch)();
void (*settingsEndBatch)();
bool (*installSplitPkg)(JNIEnv *env, const int *fds, int count, long progressId);
@@ -122,6 +138,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"));
@@ -153,6 +170,20 @@ struct RPCSXLibrary : RPCSXApi {
result.loginUser = reinterpret_cast<decltype(loginUser)>(dlsym(handle, "_rpcsx_loginUser"));
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
// Optional like the frame-gen group above: a core predating RPCN support simply has no
// such symbols, and the Kotlin side treats a null as "this build cannot do RPCN".
result.rpcnGetConfig = reinterpret_cast<decltype(rpcnGetConfig)>(dlsym(handle, "_rpcsx_rpcnGetConfig"));
result.rpcnSetConfig = reinterpret_cast<decltype(rpcnSetConfig)>(dlsym(handle, "_rpcsx_rpcnSetConfig"));
result.rpcnCreateAccount = reinterpret_cast<decltype(rpcnCreateAccount)>(dlsym(handle, "_rpcsx_rpcnCreateAccount"));
result.rpcnResendToken = reinterpret_cast<decltype(rpcnResendToken)>(dlsym(handle, "_rpcsx_rpcnResendToken"));
result.rpcnSendResetToken = reinterpret_cast<decltype(rpcnSendResetToken)>(dlsym(handle, "_rpcsx_rpcnSendResetToken"));
result.rpcnResetPassword = reinterpret_cast<decltype(rpcnResetPassword)>(dlsym(handle, "_rpcsx_rpcnResetPassword"));
result.rpcnTestLogin = reinterpret_cast<decltype(rpcnTestLogin)>(dlsym(handle, "_rpcsx_rpcnTestLogin"));
result.rpcnAddHost = reinterpret_cast<decltype(rpcnAddHost)>(dlsym(handle, "_rpcsx_rpcnAddHost"));
result.rpcnDelHost = reinterpret_cast<decltype(rpcnDelHost)>(dlsym(handle, "_rpcsx_rpcnDelHost"));
result.rpcnResetHosts = reinterpret_cast<decltype(rpcnResetHosts)>(dlsym(handle, "_rpcsx_rpcnResetHosts"));
result.rpcnSetIpv6 = reinterpret_cast<decltype(rpcnSetIpv6)>(dlsym(handle, "_rpcsx_rpcnSetIpv6"));
result.rpcnStatus = reinterpret_cast<decltype(rpcnStatus)>(dlsym(handle, "_rpcsx_rpcnStatus"));
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
@@ -263,6 +294,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
@@ -1062,3 +1107,169 @@ Java_net_rpcsx_RPCSX_frameGenShaderError(JNIEnv *env, jobject) {
const char *msg = rpcsxLib.frameGenShaderError ? rpcsxLib.frameGenShaderError() : "";
return env->NewStringUTF(msg ? msg : "");
}
// ---- RPCN ----
//
// Every one of these blocks on the network; the Kotlin side calls them off the UI thread.
// A null pointer means the core predates RPCN support, which is reported as a message
// rather than a crash so an old core degrades to "unavailable" instead of taking the app
// down.
static jstring rpcn_unavailable(JNIEnv *env) {
return env->NewStringUTF("This build of the emulator core has no RPCN support.");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnGetConfig(JNIEnv *env, jobject) {
if (!rpcsxLib.rpcnGetConfig) return env->NewStringUTF("");
const char *json = rpcsxLib.rpcnGetConfig();
return env->NewStringUTF(json ? json : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnStatus(JNIEnv *env, jobject) {
if (!rpcsxLib.rpcnStatus) return env->NewStringUTF("");
const char *json = rpcsxLib.rpcnStatus();
return env->NewStringUTF(json ? json : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnAddHost(JNIEnv *env, jobject, jstring desc, jstring host) {
if (!rpcsxLib.rpcnAddHost) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string d = str(desc), h = str(host);
const char *msg = rpcsxLib.rpcnAddHost(d, h);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnDelHost(JNIEnv *env, jobject, jstring desc, jstring host) {
if (!rpcsxLib.rpcnDelHost) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string d = str(desc), h = str(host);
const char *msg = rpcsxLib.rpcnDelHost(d, h);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT void JNICALL
Java_net_rpcsx_RPCSX_rpcnResetHosts(JNIEnv *, jobject) {
if (rpcsxLib.rpcnResetHosts) rpcsxLib.rpcnResetHosts();
}
extern "C" JNIEXPORT void JNICALL
Java_net_rpcsx_RPCSX_rpcnSetIpv6(JNIEnv *, jobject, jboolean enabled) {
if (rpcsxLib.rpcnSetIpv6) rpcsxLib.rpcnSetIpv6(enabled == JNI_TRUE);
}
extern "C" JNIEXPORT void JNICALL
Java_net_rpcsx_RPCSX_rpcnSetConfig(JNIEnv *env, jobject, jstring host, jstring npid,
jstring password, jstring token) {
if (!rpcsxLib.rpcnSetConfig) return;
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string h = str(host), n = str(npid), p = str(password), t = str(token);
rpcsxLib.rpcnSetConfig(h, n, p, t);
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnCreateAccount(JNIEnv *env, jobject, jstring npid,
jstring password, jstring onlineName,
jstring email) {
if (!rpcsxLib.rpcnCreateAccount) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string n = str(npid), p = str(password), o = str(onlineName), e = str(email);
const char *msg = rpcsxLib.rpcnCreateAccount(n, p, o, e);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnResendToken(JNIEnv *env, jobject, jstring npid,
jstring password) {
if (!rpcsxLib.rpcnResendToken) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string n = str(npid), p = str(password);
const char *msg = rpcsxLib.rpcnResendToken(n, p);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnSendResetToken(JNIEnv *env, jobject, jstring npid,
jstring email) {
if (!rpcsxLib.rpcnSendResetToken) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string n = str(npid), e = str(email);
const char *msg = rpcsxLib.rpcnSendResetToken(n, e);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnResetPassword(JNIEnv *env, jobject, jstring npid, jstring token,
jstring password) {
if (!rpcsxLib.rpcnResetPassword) return rpcn_unavailable(env);
auto str = [&](jstring s) -> std::string {
if (!s) return {};
const char *c = env->GetStringUTFChars(s, nullptr);
std::string out = c ? c : "";
if (c) env->ReleaseStringUTFChars(s, c);
return out;
};
const std::string n = str(npid), t = str(token), p = str(password);
const char *msg = rpcsxLib.rpcnResetPassword(n, t, p);
return env->NewStringUTF(msg ? msg : "");
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_rpcnTestLogin(JNIEnv *env, jobject) {
if (!rpcsxLib.rpcnTestLogin) return rpcn_unavailable(env);
const char *msg = rpcsxLib.rpcnTestLogin();
return env->NewStringUTF(msg ? msg : "");
}
@@ -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")
}
}
@@ -73,6 +73,7 @@ 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.
@@ -522,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
@@ -24,6 +24,45 @@ object GameDefaults {
// the node by hand.
"BCUS98233" to mapOf("Core@@Stub PPU Traps" to "1"),
"BCES01175" to mapOf("Core@@Stub PPU Traps" to "1"),
// Yakuza: Dead Souls. Runs at 1fps with FIFO reordering on -- not slowly, but in
// one-second steps: the RSX blocks on nv406e::semaphore_acquire until the wait times
// out, draws, and does it again. 145 timeouts in one session, all on semaphore
// 0x50300FE0, while the GPU itself was doing 3.06 ms of work per frame. The acquire
// consistently outruns the release that should satisfy it -- awaited 0x68 against a
// last_observed of 0x60 -- from the very first frame onward.
//
// Turning the flattener off clears it completely and the game boots and plays.
//
// Cause not established. The obvious candidate does not hold: flattening_helper only
// drops registers marked always_ignore, and that set is four INVALIDATE methods with
// no semaphore among them -- a semaphore release hits the default branch and flushes
// the batch, which is the safe path. So this is an empirical per-title workaround
// rather than a fix, and the real mechanism is still open.
//
// The other two are for a SECOND failure, further in: the FIFO desyncs and reads a RET
// with an empty call stack -- 19 of them in one session, last cmd 0x20000 every time --
// recover_fifo() resets it each time, and eventually gives up and kills the RSX thread
// outright ("Dead FIFO commands queue state"). The game then sits at 0 fps with audio
// still playing perfectly, because everything except the renderer is still alive. The
// stray semaphore acquires that time out alongside it are downstream of the same thing:
// a desynced FIFO never runs the release that would satisfy them.
//
// These two are what the fatal message itself recommends, and they work. Which of the
// two is doing the work is not established -- both were changed at once and the game
// has not been A/B'd since -- so both are kept. Ordered & Atomic is the likelier of the
// pair given the symptom, and both cost performance, which is why they are scoped to
// this title rather than turned on globally.
"BLUS30826" to mapOf(
"Video@@Disable FIFO Reordering" to "true",
"Core@@RSX FIFO Fetch Accuracy" to "\"Ordered & Atomic\"",
"Video@@Driver Wake-Up Delay" to "20",
),
"NPUB31509" to mapOf(
"Video@@Disable FIFO Reordering" to "true",
"Core@@RSX FIFO Fetch Accuracy" to "\"Ordered & Atomic\"",
"Video@@Driver Wake-Up Delay" to "20",
),
)
/**
@@ -45,6 +84,12 @@ object GameDefaults {
private val STOCK: Map<String, String> = mapOf(
// system_config.h: cfg::_int<-64, 64> stub_ppu_traps{ this, "Stub PPU Traps", 0, true }
"Core@@Stub PPU Traps" to "0",
// system_config.h: cfg::_bool disable_FIFO_reordering{ this, "Disable FIFO Reordering", false }
"Video@@Disable FIFO Reordering" to "false",
// system_config.h: fifo_setting rsx_fifo_accuracy{ this, "RSX FIFO Fetch Accuracy", rsx_fifo_mode::atomic }
"Core@@RSX FIFO Fetch Accuracy" to "\"Atomic\"",
// system_config.h: cfg::uint<0, 16667> driver_wakeup_delay{ this, "Driver Wake-Up Delay", 0, true }
"Video@@Driver Wake-Up Delay" to "0",
)
fun forSerial(serial: String?): Map<String, String> =
@@ -195,8 +195,29 @@ data class Ps3Settings(
* off by default and the UI says so plainly. */
val silenceAllLogs: Boolean = false,
val netEnabled: Boolean = false,
val psnStatus: Boolean = false,
/** Net/PSN status: 0 = Disconnected, 1 = Simulated, 2 = RPCN.
*
* Was a Boolean, which could only ever pick Disconnected or Simulated -- so
* np_psn_status::psn_rpcn had no writer anywhere in the app and RPCN, which is fully
* compiled into the core, was unreachable. */
val psnStatus: Int = 0,
val upnpEnabled: Boolean = false,
/** The IPv4 address games are told the console has. "0.0.0.0" means "work it out". */
val ipAddress: String = "0.0.0.0",
/** Which local interface the emulated network stack binds to. "0.0.0.0" = any. */
val bindAddress: String = "0.0.0.0",
/** DNS server for the emulated stack. This is the one that matters for private/fan
* game servers: RPCN replaces Sony's PSN, but a publisher's own backend was never PSN,
* so reaching a revival of one means resolving its hostnames somewhere else. */
val dnsAddress: String = "8.8.8.8",
/** Per-hostname redirects, "host=1.2.3.4" joined by "&&" -- finer than dnsAddress
* because it moves one hostname instead of every lookup. Parsed by np::dnshook. */
val ipSwapList: String = "",
/** Derive the console's MAC from its PSID rather than using a fixed one. */
val deriveMacFromPsid: Boolean = false,
/** Two-letter country code reported to PSN/RPCN. */
val psnCountry: String = "us",
val clansEnabled: Boolean = false,
/**
* 0 = Accurate, 1 = Approximate, 2 = Relaxed, 3 = Inaccurate.
*
@@ -736,12 +757,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 ----
@@ -1105,6 +1132,13 @@ 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/Net", "IP address", "string", ps3.ipAddress)
put("PS3/Net", "Bind address", "string", ps3.bindAddress)
put("PS3/Net", "DNS address", "string", ps3.dnsAddress)
put("PS3/Net", "IP swap list", "string", ps3.ipSwapList)
put("PS3/Net", "Derive MAC from PSID", "bool", ps3.deriveMacFromPsid.toString())
put("PS3/Net", "PSN Country", "string", ps3.psnCountry)
put("PS3/Net", "Clans Enabled", "bool", ps3.clansEnabled.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())
@@ -1275,12 +1309,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
@@ -1336,10 +1368,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.
@@ -2082,6 +2112,13 @@ data class Settings(
put("ps3NetEnabled", ps3.netEnabled)
put("ps3PsnStatus", ps3.psnStatus)
put("ps3UpnpEnabled", ps3.upnpEnabled)
put("ps3IpAddress", ps3.ipAddress)
put("ps3BindAddress", ps3.bindAddress)
put("ps3DnsAddress", ps3.dnsAddress)
put("ps3IpSwapList", ps3.ipSwapList)
put("ps3DeriveMacFromPsid", ps3.deriveMacFromPsid)
put("ps3PsnCountry", ps3.psnCountry)
put("ps3ClansEnabled", ps3.clansEnabled)
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
put("ps3ConsoleLanguage", ps3.consoleLanguage)
put("ps3ConsoleRegion", ps3.consoleRegion)
@@ -2427,8 +2464,20 @@ data class Settings(
audioBuffering = json.optBoolean("ps3AudioBuffering", def.ps3.audioBuffering),
audioBufferMs = json.optInt("ps3AudioBufferMs", def.ps3.audioBufferMs),
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
// optInt with a Boolean fallback for installs written before this was a
// tri-state: a stored `true` reads back as 1 (Simulated), which is what it
// meant.
psnStatus = if (json.opt("ps3PsnStatus") is Boolean)
(if (json.optBoolean("ps3PsnStatus")) 1 else 0)
else json.optInt("ps3PsnStatus", def.ps3.psnStatus),
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
ipAddress = json.optString("ps3IpAddress", def.ps3.ipAddress),
bindAddress = json.optString("ps3BindAddress", def.ps3.bindAddress),
dnsAddress = json.optString("ps3DnsAddress", def.ps3.dnsAddress),
ipSwapList = json.optString("ps3IpSwapList", def.ps3.ipSwapList),
deriveMacFromPsid = json.optBoolean("ps3DeriveMacFromPsid", def.ps3.deriveMacFromPsid),
psnCountry = json.optString("ps3PsnCountry", def.ps3.psnCountry),
clansEnabled = json.optBoolean("ps3ClansEnabled", def.ps3.clansEnabled),
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
consoleLanguage = json.optInt("ps3ConsoleLanguage", def.ps3.consoleLanguage),
consoleRegion = json.optInt("ps3ConsoleRegion", def.ps3.consoleRegion),
@@ -2756,6 +2805,13 @@ 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.ipAddress != base.ps3.ipAddress) j.put("ps3IpAddress", current.ps3.ipAddress)
if (current.ps3.bindAddress != base.ps3.bindAddress) j.put("ps3BindAddress", current.ps3.bindAddress)
if (current.ps3.dnsAddress != base.ps3.dnsAddress) j.put("ps3DnsAddress", current.ps3.dnsAddress)
if (current.ps3.ipSwapList != base.ps3.ipSwapList) j.put("ps3IpSwapList", current.ps3.ipSwapList)
if (current.ps3.deriveMacFromPsid != base.ps3.deriveMacFromPsid) j.put("ps3DeriveMacFromPsid", current.ps3.deriveMacFromPsid)
if (current.ps3.psnCountry != base.ps3.psnCountry) j.put("ps3PsnCountry", current.ps3.psnCountry)
if (current.ps3.clansEnabled != base.ps3.clansEnabled) j.put("ps3ClansEnabled", current.ps3.clansEnabled)
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)
@@ -3062,8 +3118,19 @@ data class Settings(
audioBuffering = if (overrides.has("ps3AudioBuffering")) overrides.getBoolean("ps3AudioBuffering") else base.ps3.audioBuffering,
audioBufferMs = if (overrides.has("ps3AudioBufferMs")) overrides.getInt("ps3AudioBufferMs") else base.ps3.audioBufferMs,
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
psnStatus = if (overrides.has("ps3PsnStatus"))
(if (overrides.opt("ps3PsnStatus") is Boolean)
(if (overrides.getBoolean("ps3PsnStatus")) 1 else 0)
else overrides.getInt("ps3PsnStatus"))
else base.ps3.psnStatus,
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
ipAddress = if (overrides.has("ps3IpAddress")) overrides.getString("ps3IpAddress") else base.ps3.ipAddress,
bindAddress = if (overrides.has("ps3BindAddress")) overrides.getString("ps3BindAddress") else base.ps3.bindAddress,
dnsAddress = if (overrides.has("ps3DnsAddress")) overrides.getString("ps3DnsAddress") else base.ps3.dnsAddress,
ipSwapList = if (overrides.has("ps3IpSwapList")) overrides.getString("ps3IpSwapList") else base.ps3.ipSwapList,
deriveMacFromPsid = if (overrides.has("ps3DeriveMacFromPsid")) overrides.getBoolean("ps3DeriveMacFromPsid") else base.ps3.deriveMacFromPsid,
psnCountry = if (overrides.has("ps3PsnCountry")) overrides.getString("ps3PsnCountry") else base.ps3.psnCountry,
clansEnabled = if (overrides.has("ps3ClansEnabled")) overrides.getBoolean("ps3ClansEnabled") else base.ps3.clansEnabled,
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,

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