260 Commits
Author SHA1 Message Date
jpolo1224 dca685b67d Android: 0.9.4.3 (versionCode 23) 2026-08-23 02:51:07 -04:00
jpolo1224 78650f32d8 Savers: always run gl1_init(), never trust a stale g_started
port_new opened with "if (g_started) return 1". That was defensible while
gl1's state was whatever the previous saver left behind, but nativeInit now
calls gl1_lost() before every create, so gl1 is guaranteed DOWN on entry.
Reporting success there would hand the caller a saver with no shim under it.

So the gl1_lost() added alongside the leak fix did not just close the leak,
it turned that early return from redundant into wrong -- its safety rested
on a call-graph property that same change removed. All six ports now tear
down a stale run through port_free and always run gl1_init(). Each needs a
forward declaration of port_free, which is defined below port_new.

No behaviour change on any path reachable today: port_free always clears
g_started, so the guard never fires. The point is that a saver added later,
or an upstream cleanup that returns early, should fail in its own saver
rather than poison the next one -- the same reason port_free tears gl1 down
unconditionally.

Adapted from the ARMSX2 change (be674a64c5).
2026-08-23 02:44:04 -04:00
jpolo1224 24951bb863 Fix animated background locking users out of the app
Ported from the ARMSX2 fix; ARMSX3 had the same bug, plus a sixth saver.

A saver that dies natively made the app unlaunchable. The choice is a
persisted pref read on the library screen -- the first screen -- so the
crash repeated on every launch and Settings was never reachable to turn it
off. The only escape was clearing app data, which takes memory cards and
save states with it.

Cause: gl1's state is a file-scope global holding GL object names, and
gl1_init() early-returns on g.ready. Skyrocket and Lattice defer initSaver()
to port_resize, so a create-then-teardown with no surface size left
g_started false and their port_free returned BEFORE gl1_shutdown(); flux,
plasma and solarwinds leaked it the other way, since returning 0 when
initSaver() leaves readyToDraw clear means port_free is never called at all.
Either way g.ready stayed set with names from a destroyed EGL context, and
the next saver -- new view, new context -- drew against them. Drivers answer
that with anything from a black screen to a segfault.

Every port now gives gl1 back on every exit, unconditionally, so the rule
holds by construction rather than by arguing about which paths are
reachable. gl1_lost() already existed here but was never called; nativeInit
calls it now as the standing invariant, so a new context cannot inherit old
GL names even if a saver added later forgets.

Containment, independent of the fix: native GL can always find a new way to
die, so the setting arms itself with a synchronous commit() before the
render thread starts and disarms when that thread exits in an orderly way.
Still armed at startup means the last run died with a saver up -- the
background switches off and a toast names it. A crash or a kill cannot reach
the disarm; that is the entire signal.

Also guards Thread.start(): it asks for a 16MB stack (Skyrocket declares a
3MB starmap as a local) and an OutOfMemoryError there is an uncaught throw
on the main thread -- the same lockout with no native crash involved.
2026-08-23 02:15:41 -04:00
jpolo1224 620eaf5066 Oboe: correct a device that opens at the wrong sample rate
The backend asked for the guest's rate with sample rate conversion
disabled, then recorded the rate it ASKED for rather than the one the
stream actually opened at. A device that cannot run at the guest rate
opens at its own, and with conversion off nothing resamples -- so the
emulator feeds samples at one rate into a device consuming them at
another. That is a permanent rate mismatch, not a one-off pitch error:
the gap between what is heard and what is on screen grows for as long as
playback continues, and nothing downstream can see it, because every
later calculation (including the buffering algorithm's idea of how much
audio is queued) uses the rate we requested.

Check the opened rate and, on a mismatch, reopen letting Oboe convert.
Converting is a worse signal path than matching rates outright and a far
better one than not converting at all; Cubeb resamples for the same
reason. If the rate is still wrong after that, say so in the log rather
than drifting in silence.

Refs #87
2026-08-23 01:52:24 -04:00
jpolo1224 5d91f8c56b Say why an ISO failed to boot instead of a bare error
load_iso() cannot report failure: it mounts the virtual device whether or
not the archive actually opened. An image that is unreadable, truncated,
or encrypted without a usable disc key therefore sailed past the mount and
died much further down as a generic 'invalid file or folder', with nothing
in the log distinguishing it from a dozen other causes. A bug report of
that failure carries no information at all.

Check the mount where the reason is still knowable. An image whose
filesystem did not parse now reports whether a disc key was missing or
rejected -- returning decryption_error, and naming both the key locations
searched -- or, failing that, says the image looks truncated. When the
archive is readable but has no EBOOT.BIN at the expected path, log what the
disc root did contain; install discs legitimately lack one, so that stays a
warning rather than a failure.

Also realign BootResult with game_boot_result. It was missing
firmware_version and database_config_missing, so every code from ordinal 10
down was reported as its neighbour -- still_running surfaced as
'AlreadyAdded' -- and the last two had no entry at all, making fromInt
throw rather than return. fromInt is now total.

Refs #88
2026-08-23 01:47:38 -04:00
jpolo1224 06b33abf27 Send rumble to the controller, not the phone
Rumble always went to the phone's own motor: the vibrator lookup asked
the system service and never considered the connected pad. On a handheld
that is the wrong motor outright, and on a phone-plus-controller setup it
buzzes the device sitting in a dock while the pad in hand stays still.

Prefer the first connected gamepad or joystick reporting a working motor,
falling back to the phone. The target is resolved per state change rather
than cached for the pump's lifetime, so connecting or disconnecting a pad
mid-session moves rumble with it; the motor that was last started is
tracked separately so unplugging mid-rumble cannot leave one buzzing.

Add a 'Vibrate the phone' toggle gating only that fallback, so playing on
a pad need not mean the phone rumbles along with it. Touch haptics stay
on the phone deliberately -- the finger is on the phone's screen.

Closes #89
2026-08-23 01:39:03 -04:00
jpolo1224 daed55c427 Release: 0.9.4.2 2026-08-22 10:09:25 -04:00
jpolo1224 43d26f3720 FrameGen: flush the generated blit before presenting it, and refuse multithreaded RSX
present_generated_frame submitted its blit and presented immediately.
command_buffer::submit goes through queue_submit, which DEFERS to the offloader
thread when multithreaded RSX is on -- so the present waited on a semaphore whose
signal operation had not been submitted, which the spec forbids and which hangs
the present without returning the acquired image. The real present path flushes
first for exactly this reason; this now does the same.

Frame generation also refuses to run at all with multithreaded RSX enabled.
generate() submits inline on the RSX thread while the frame's own command buffer
is merely enqueued to the offloader, so the interpolation can reach the queue
before the frame it reads -- and two threads then submit to one VkQueue without
the external synchronisation vkQueueSubmit requires, which is what g_submit_mutex
exists for and what generate() bypasses by calling vkQueueSubmit directly.

Refusing is honest. Supporting it means routing through vk::queue_submit and
reasoning about deferred submission, which is not a change to make untested.
MTRSX defaults off, so this affects only users who enabled it -- and it is the
profile that produces intermittent DEVICE_LOST under load.
2026-08-22 06:27:43 -04:00
jpolo1224 30f4d93733 FrameGen: read the presented image in the layout it is actually in
capture_presented_frame() was handed target_layout -- COLOR_ATTACHMENT_OPTIMAL or
TRANSFER_DST_OPTIMAL -- and stored it for generate() to use. But generate()
records into its own command buffer, submitted after the frame's, and the frame's
own transition to present_layout runs in between. So by the time the barrier
executed, its oldLayout named a layout the image had already left, and the
matching barrier on the way out restored that same wrong layout -- leaving the
image outside PRESENT_SRC_KHR when it was presented.

Both are violations: oldLayout must be the current layout or UNDEFINED
(VUID-VkImageMemoryBarrier-oldLayout-01197), and a presented image must be in
PRESENT_SRC_KHR (VUID-VkPresentInfoKHR-pImageIndices-01430). It also desynced the
renderer's own layout bookkeeping for that swapchain image on every subsequent
frame.

Passing present_layout instead describes what the image will actually be in, so
the barrier is a no-op transition that reads it where it lies and leaves it there.

Found by a Vulkan lifetime and layout audit. Not verified on device.
2026-08-22 06:26:10 -04:00
jpolo1224 1e9f8c70da FrameGen: give the capture barrier a real source scope, and ask for TRANSFER_SRC
Two findings from the Vulkan audit, both of which let the interpolation read an
image it had no valid dependency on.

The barrier before Process used TOP_OF_PIPE with MEMORY_READ. TOP_OF_PIPE in
srcStageMask specifies no stage of execution, so the first synchronization scope
was empty and no availability operation happened for the colour-attachment and
transfer writes the frame made into that image in the PREVIOUS submission.
Submission order on a queue orders execution; it is not a memory dependency. So
the interpolation could read a partially-flushed frame -- which presents as
smearing during motion and does not respond to any interpolation setting, since
the interpolation is not what is wrong. Source scope is now
COLOR_ATTACHMENT_OUTPUT|TRANSFER with the matching write access, and the mirror
barrier's empty BOTTOM_OF_PIPE second scope becomes ALL_COMMANDS.

Separately, the swapchain was created COLOR_ATTACHMENT|TRANSFER_DST, but
FrameGen::Process copies FROM the presented image as TRANSFER_SRC_OPTIMAL. Both
that layout and vkCmdCopyImage require TRANSFER_SRC usage. Nothing else in the
renderer reads a WSI image, so it had never been needed. Requested only where the
surface reports it, with a warning otherwise, since it is not guaranteed on
Android.
2026-08-22 06:24:56 -04:00
jpolo1224 e7ab163b54 FrameGen: release its Vulkan objects before the device that owns them
Frame generation's state is file-scope global and had no teardown path.
shutdown() only finalised the old dlopen'd library, and release_shared_images()
was never called at all, so at emulation stop the fences, command pool, command
buffers, allocator, queue and the whole pass chain stayed live as children of a
destroyed VkDevice.

The reachable consequence is worse than the leak. On the NEXT boot in the same
process g_native.valid() was still true, so build_native_stack() was skipped and
generate() waited on a fence from the dead device, reset freed command buffers
and submitted to its queue. g_shared_w/h still matched too, so the output images
were not recreated either.

That is boot -> stop -> boot, and it is deterministic rather than intermittent.
It is the likeliest explanation for frame generation working in one game and then
failing immediately in the next one launched without restarting the app, which is
exactly how it was reported.

release_device_resources() destroys the stack and the images and resets the state
that gates rebuilding, including g_disabled -- a new device may not fail where the
last one did. Called from ~VKGSRender after vkDeviceWaitIdle, while the device is
still alive.

Found by a Vulkan lifetime audit. Not verified on device; the hardware was
unavailable.
2026-08-22 06:22:39 -04:00
jpolo1224 e66ce8328a FrameGen: stop reading the shader cache twice per frame, and fix the output image layouts
shader_count() read the ENTIRE SPIR-V cache off disk and rebuilt every module,
then threw the result away -- and it ran twice per frame on the RSX thread, from
capture_presented_frame() and generated_frame_count(). At ~322KB that is roughly
644KB of file IO, four stats, two opens and hundreds of allocations per frame, on
the thread driving the whole present path.

That is CPU cost, which is why halving the GPU shader cost with fp16 changed
nothing, and why ~3ms of measured interpolation was costing ~20ms of frame time.
It arrived with the fix for shaders not loading at all: making the probe ask the
cache was right, doing it per frame was not. The verdict can only change on
import, so it is memoised and cleared there, as ARMSX2 does for the same reason
(GSLsfg.cpp:53-58, whose comment names this exact hazard).

Also: GenerateInto writes through a storage image view, which requires
VK_IMAGE_LAYOUT_GENERAL, and the blit that follows declares TRANSFER_SRC. Nothing
transitioned those images either way -- a storage write outside GENERAL is
undefined, and on Adreno a mismatched layout on a UBWC image forces a
conservative full-surface decompress. Both transitions added, with the layout
tracked across frames.

And the generated-frame blit is source-size to destination-size, so LINEAR
filtering bought nothing and cost a filtered sampling pass instead of the fast
copy path. NEAREST.

Found by a comparative audit against the ARMSX2 driver. Not yet verified on
device -- the hardware was unavailable when this was written.
2026-08-22 06:19:06 -04:00
jpolo1224 a378f38ba6 FrameGen: never stall a frame waiting for the previous interpolation
The fence wait at the head of generate() blocked for up to a second on the
previous slot's work. When it actually waited, the long frame interval it
produced went straight into the pacer -- whose clock is sampled after this point
-- and any interval over 100ms trips MINIMUM_BASE_RATE and stops generation for a
full second (FrameGenPacer.cpp:29/40/97-100).

So a stall here made the pacer stand down, and it was measuring a rate we had
slowed ourselves. Measured against that: the pacer asked for a generated frame on
only ~40% of real frames, which matches one >100ms interval every couple of
seconds costing a second of generation each time.

A zero timeout asks whether the previous frame's work is done and skips this
frame if it is not. Skipping costs one generated frame; stalling cost a second of
them. With three rotating slots the work has three frames to finish, and it
measures ~3ms, so skips should be rare.

VK_TIMEOUT is now a normal outcome rather than an error, so only real failures --
VK_ERROR_DEVICE_LOST being the one actually suspected here -- reach the counter
that disables the feature.
2026-08-22 06:14:00 -04:00
jpolo1224 f2bce78ba1 FrameGen: report what the fence wait actually returned
Every non-success result from vkWaitForFences was reported as a timeout, which
hid the one detail that mattered: 60 of them arrived in 3 seconds against a
1-second timeout, so the wait was failing immediately rather than expiring.
VK_ERROR_DEVICE_LOST is the obvious candidate and means something entirely
different from a slow frame -- the device is gone and frame generation is the
messenger, not the cause.

The failure is intermittent, so this logs the result code once per stall rather
than trying to fix a cause that has not been identified.
2026-08-22 06:00:40 -04:00
jpolo1224 f55a246f22 FrameGen: revert the single-submit restructure, keep the resolution and timeout fixes
The single-submit path broke two games -- one reported "failed", another stopped
progressing past its logo -- and reverting it restores both. It was chasing ~8 ms
of measured per-submit overhead, and the idea may still be right, but it changed
swapchain image ownership incrementally and got it wrong four separate ways:
acquiring before the count was known (leaving semaphores signalled and every
later acquire failing), sharing semaphores across in-flight frames, leaking
acquired images that were never presented, and writing storage images outside
GENERAL. That is a design that needs settling before it is written, not patched
into place against a device.

Kept, all measured:

- The acquire semaphore. Generated frames were acquired with VK_NULL_HANDLE and
  written immediately -- a swapchain image used before the presentation engine
  had finished with it. That was the judder: invariant to every interpolation
  setting because the interpolation was never at fault, present at any frame
  rate because a race has no cadence, and smearing rather than stutter because a
  torn image is parts of two frames at once. Gone entirely at x2.

- The guest extent. The passes size their optical flow from the ratio between
  what the game rendered and what is presented; passing the presented size for
  both made that ratio 1.0 and ran the flow at full 1080p for a title that
  rendered 720p. 4.8 ms -> 3.0 ms of GPU time.

- fp16 shaders, where the device reports and enables them.

- Fence timeouts are transient. disable() is permanent for the session, so one
  stall during a load spike left frame generation reading "failed" until the
  game was restarted with nothing wrong.

Frame generation costs ~3 ms of GPU time per frame. On a title already at the
refresh rate that is enough to cross a vblank boundary, so it trades real frames
for interpolated ones and gains nothing; it is worth having where a game has
headroom below the panel rate, not where it is already at the cliff edge.
2026-08-22 05:54:58 -04:00
jpolo1224 8759a648f7 FrameGen: one command buffer and one submit, as ARMSX2 does
Measured, not guessed: a GPU timer around the passes says they cost 4.1 ms per
frame, steady, while frame time went from ~17 ms to ~30 ms with generation on.
Roughly 8 ms per frame was therefore not interpolation -- it was the cost of
submitting it.

ARMSX2 records Process, every GenerateInto, and every copy into the acquired
swapchain images into ONE command buffer and issues ONE vkQueueSubmit that waits
on each acquire and signals each present. ARMSX3 submitted framegen's work and
then a separate command buffer and submit per generated frame, each its own GPU
batch on the same queue with its own scheduling gap.

generate_into_targets() takes the acquired images and does the whole frame in one
buffer and one submit; the present path acquires them up front and issues the
presents afterwards. present_generated_frame is left in place for the pipelined
path, which is still disabled, and no longer runs on this one.

Follows the acquire-semaphore fix in the previous commit -- that was the judder
(a swapchain image written before the presentation engine had finished with it),
this is the cost.
2026-08-22 05:20:43 -04:00
jpolo1224 5dc669bc7f FrameGen: acquire generated frames with a semaphore instead of nothing
present_generated_frame acquired its swapchain image with VK_NULL_HANDLE -- no
semaphore, no fence -- and then blitted into it and presented immediately.
Vulkan requires waiting on the acquire before touching the image, so this was
writing into one the presentation engine could still be scanning out.

That accounts for what nothing else did. The artifact was invariant to flow
scale, shader family, fp16 and multiplier, because all of those change how a
pair is interpolated and the interpolation was never at fault. It appeared at
every frame rate, because a race has no cadence. It looked like smearing rather
than stutter, because a torn image is parts of two frames at once, and it only
showed during motion, because that is when consecutive frames differ.

The real frame has always had an acquire/present semaphore pair in
frame_context_t. Only the generated path went without, which is why nothing
inside frame generation could reveal it.

ARMSX2 acquires with a fence and blocks on it before submitting, and says so.
This waits on the GPU instead: the blit waits on the acquire, the present waits
on the blit, one pair per swapchain image so none is reused while in flight.

Also restores the fp16 probe consistency in shader_count(), which a git checkout
during this session reverted -- three callers ask which shader variant to use and
any two disagreeing makes a freshly imported cache read as "no shaders".
2026-08-22 05:12:48 -04:00
jpolo1224 11f960fde9 FrameGen: use the fp16 shaders, which this device has and ARMSX2's hardcode denied it
Frame generation was running the fp32 shader family on hardware that supports
fp16. Measured on an Adreno 740: real frame rate fell from ~60 to ~25 with
generation on, so the output was worse than not using it at all.

The cause was copying a hardcode without its reason. ARMSX2 sets allow_fp16 and
prefer_fp16 false and says why: PCSX2's Vulkan backend never asks for
shaderFloat16 when it creates its logical device, so a module declaring the
Float16 capability would be invalid usage there -- and its own comment ends
"restore Eden's two lines if PCSX2 ever enables the feature". Eden does gate on
the device, and Eden is the one that performs. RPCS3 enables shaderFloat16 where
the driver is trusted, and clears its own flag on the Adreno drivers whose
compiler rejects native float16, so asking the device is both possible and safe.
The device in question logs "GPU/driver supports float16 data types natively".

The compat Device answers a third query now, as Eden's does. BuildShaderCache
takes the flags rather than choosing its own, because the cache header stores
them and rejects an entry whose flags differ -- the same mismatch that made a
freshly written cache read as "no shaders".

Found by measuring instead of reasoning: the CPU side of the passes costs
0.000-0.061 ms/frame, which said the cost was GPU-side and not in how the work
was sequenced, after four attempts at the sequencing.
2026-08-22 04:19:21 -04:00
jpolo1224 c537563d14 FrameGen: rotate command buffers so a frame stops waiting on the one before it
One command buffer and one fence meant every frame waited for the immediately
previous interpolation to finish before it could reset the buffer. The GPU
pipeline drained once per frame and the CPU could never get ahead, which is a
stall no amount of moving the work around removes -- it is the depth that is
wrong, not the placement.

ARMSX2 keeps one slot per swapchain image, each with its own command buffer and
fence, and waits on the fence belonging to the slot it is about to reuse. That
fence is several frames old, so in the normal case it is already signalled and
the wait costs nothing. Three slots here, rotated per frame.

This is the last of the differences found by reading the ARMSX2 driver rather
than reasoning about my own: the copy, then the pacer's clock, then the frame's
own command buffer, and now the depth.

Discord: the emulator logo now appears beside the cover art, as it does in
ARMSX2. By URL rather than asset key -- a key that is not in the portal makes
Discord reject the entire presence update, which is what left the card blank
before. It is the launcher icon out of this repository, so it cannot drift from
the app. With no cover the logo takes the large slot instead of sending no assets
at all.
2026-08-22 04:14:13 -04:00
jpolo1224 f8b4d63323 FrameGen: keep the interpolation off the frame's own command buffer
The previous commit removed the capture copy by recording Process() into the
frame's command buffer. That traded one problem for a worse one: the present path
waits on that buffer, so the interpolation's GPU cost came straight off the real
frame rate. Measured on device, 1920x1080 content ran at 24-28 real fps with
generation on, while the CPU side of the same call cost 0.013-0.061 ms -- the
work was not expensive to record, it was expensive to be waited on.

ARMSX2 runs Process in framegen's OWN command buffer, after the frame, against
the same presented image. That costs neither the copy nor the frame, and it is
what this now does: the capture side only remembers the image and lets the pacer
plan, and generate() transitions it, runs Process, runs GenerateInto per planned
frame, and submits -- one buffer, one submission, off the critical path.

Defaults were checked against ARMSX2 first and are identical (flow scale 100,
performance shaders on, target rate 0, multiplier 2), so this was never a settings
difference.
2026-08-22 04:06:54 -04:00
jpolo1224 eda6870a47 FrameGen: process the real frame in the frame's own command buffer, like ARMSX2 does
Two defects, both from grafting the passes onto the existing shape instead of
adopting the one they were written for.

A copy per frame that ARMSX2 never makes. Process() copies the source into its
chain itself (FrameGen.cpp, CopyPresentedFrame), and ARMSX2 hands it the real
swapchain image. This handed it a capture copy made by a separate full-frame blit,
so every frame paid for two 1920x1080 copies instead of one. The capture existed
only because generation happened later, from another submission, by which time the
swapchain image was gone -- so the copy was paying for a problem the ordering
created.

And the pacer planned against a clock that jittered. Plan() derives the base frame
rate from the interval between its own calls; it sat inside generate(), behind
three early returns and a conditional call site, so it measured an interval the
game never had and planned against it. That is judder by construction, and no
amount of tuning the multiplier reaches it.

Process now runs in the frame's OWN command buffer, against the presented image,
where the frame is finished and the image is still ours -- which removes the copy
and the reason for it together. The pacer plans there too, once per frame,
unconditionally. generate() keeps only the generation half: record GenerateInto
per planned frame, submit, and let queue order carry it into the blits.

The input capture images are gone entirely.

Reported as judder and lower performance against ARMSX2 and Eden running the same
passes, which was the right comparison to make.
2026-08-22 04:00:33 -04:00
jpolo1224 b4a552d697 FrameGen: take the interpolation off the critical path, and fix the shader cache flags
The cache was written with one flag set and read with another. BuildShaderCache
asked for allow_fp16 = true while LsfgShaders, the only consumer, asks for
(false, false), and the header stores those flags and is rejected when they
differ -- so the cache written at import could never satisfy the read after it.
On a desktop that is invisible: the source DLL is still there, LoadShaderModules
falls through to re-parsing the executable, and the cache is dead weight rewritten
every launch. Android has nothing to fall through to, because the picked file is
a copy in app cache that the system may clear, so it surfaced as "no shaders"
with a valid cache sitting next to it. Worth telling Camille: Eden and ARMSX2 are
both re-parsing Lossless.dll every launch rather than using their cache.

The cache also survives a missing source now. It is validated against the source's
size and mtime, which is right where an install stays put and wrong here; passing
0 skips those checks, matching what source_hash and variant already do. A replaced
Lossless Scaling is no longer noticed automatically, which is the trade -- against
losing the cache to a routine cache sweep, and re-importing is explicit.

Performance: the fence wait moved from after the submit to before the next one.
Waiting at the end put the whole interpolation on the critical path -- the thread
sat idle until the GPU finished, every frame -- when nothing required it: frame
generation and the present blits share one queue, so submission order already
orders them. Waiting at the START only blocks when the previous frame's passes
have not finished in time. The queue is now taken from the device rather than a
second vkGetDeviceQueue, so that sharing is explicit rather than incidental.

Settings: motion detail is a slider rather than three stops, and performance
shaders is a switch rather than a pair of buttons.
2026-08-22 03:51:34 -04:00
jpolo1224 caaadbe266 FrameGen: fix the allocator crash and the shader gate, and expose the rest of the settings in-game
Three defects from the switchover, all mine.

vmaCreateAllocator crashed at pc 0 the first time frame generation was switched
on. The allocator was created with physicalDevice and device but no instance and
no vulkanApiVersion; Android builds with VK_NO_PROTOTYPES, so VMA resolves its
table dynamically through vkGetInstanceProcAddr, which answers only for global
functions when the instance is null. It stored nulls and called one. memory.cpp
documents this exact failure at its own vmaCreateAllocator -- the two getters were
supplied and the instance beside them was not.

shader_count() then answered for the wrong cache, twice. First it still counted
g_shaders, which import_shaders stopped filling when it moved to the ported
extractor: generated_frame_count() multiplies by it, so generate() returned on its
first line and frame generation sat on "starting" with nothing logged, because it
never got far enough to fail. Replacing that with GetInstalledLosslessStatus() was
also wrong -- that validates the DLL PATH, and the UI copies the picked file into
app cache, which Android may purge. A re-import wrote a good 322KB shader cache and
it still said "no shaders". It now asks whether the cache LOADS, which is the
actual question: the shaders are extracted once and the source is not needed again.

The dead initialize() call in generate() is gone with it. It built the dlopen'd
library's context, nothing uses it, and a failure there could still disable the
feature.

Settings: the in-game menu offered the multiplier alone. Someone opening it
mid-game is there to answer "it is generating but the picture is unsteady" or
"it is generating but it is too expensive", and neither is the multiplier. Target
rate, flow scale and performance mode now sit beside it. Target rate is new --
0 keeps the fixed multiplier, non-zero is Camille's adaptive pacing.
2026-08-22 03:39:59 -04:00
jpolo1224 b5f449ccb6 FrameGen: run the ported passes, and stop going through the separate device
Wires up the previous commit. Frame generation now runs on OUR VkDevice, in our
own present path, instead of inside a dlopen'd library with a VkDevice of its own.

What that removes: every frame used to cross between devices as an
AHardwareBuffer -- allocated, imported as a VkImage on our device so the renderer
could blit into it, handed over as a raw buffer, imported again on theirs. The
capture images are plain device-local VkImages now, because the passes read the
very images the renderer already wrote. The AHardwareBuffer external-memory
extension was also the narrowest gate on the whole feature and is no longer
required.

Kept deliberately:

- The capture itself. The swapchain image is presented and reused, so the passes
  still need a stable copy; only its backing changed.
- commit_capture(), now near-vestigial. It existed because a second VkDevice had
  no semaphore joining it to ours. One device and one queue means submission
  order already says this, but the call site is where the capture becomes
  readable and that is worth keeping named.
- The fence wait in generate(). The present path blits the generated images
  immediately after, so the old contract -- return only when they are ready --
  still holds. A semaphore would do it without stalling the thread and is the
  obvious next step, on a path that has broken three times from partial fixes.

Frame generation records into its OWN command buffer, as the ARMSX2 driver does:
the passes have to run after the captured frame is complete and before the
generated images are blitted, and the renderer's buffer is already closed by then.

import_shaders now drives the ported extractor rather than the library. Both kept
their own copy of the shaders, and importing through the library would have looked
like it worked -- a count comes back, the settings screen agrees -- while the cache
the passes actually read stayed empty. Its failures are now phrased for the person
who chose the file.

VKPresent.cpp is untouched: generate(), generated_frame_count(), generated_image(),
capture_presented_frame() and commit_capture() all kept their signatures and their
meaning, which is the whole reason the switchover is one commit and not five.

Builds clean. NOT yet run on hardware -- neither this nor the device-feature change
under it has drawn a frame.
2026-08-22 03:19:55 -04:00
jpolo1224 e05ea4d219 FrameGen: port Camille's native LSFG implementation from ARMSX2
Compiles; not yet wired. VKFrameGen still drives the old dlopen path and nothing
calls these passes -- that is the next commit. Landing it here keeps the port and
the switchover separable, because the present path is the part that has broken
before.

The implementation is Camille LaVey's, from eden-emu PR #4263, by way of ARMSX2
2.6.6.8. She licensed it GPL-2.0-or-later for use here: ARMSX3 derives from
RPCS3, which is GPL-2.0-ONLY with no "or later" clause, so the original
GPL-3.0-or-later terms could not be carried across. Every file records that.
Eden is unaffected -- "or later" leaves its own use exactly as it was.

Why it is worth having: the current implementation runs behind a dlopen'd .so
with its OWN VkDevice, so every frame crosses devices as an AHardwareBuffer --
allocated, imported on our device, handed over, imported again on theirs. These
passes run on our device in our own present path and that round-trip disappears.
It also brings adaptive pacing: instead of a fixed x2, generate as many frames as
it takes to hold a target refresh rate, which is the answer to a game oscillating
between 60 and 30 on a 60Hz panel.

Porting notes, all of it concentrated in three seams rather than spread:

- LsfgVkCompat's Device adapter wrapped GSDeviceVK and now wraps vk::render_device.
  28 of the 31 files never mention either and were not touched.
- GSConfig lives in FrameGenConfig.h, mapping RPCS3's mode enum and cfg fields to
  the names the passes read, so a later merge from Eden touches passes not wiring.
- LosslessDll's platform calls (FileSystem/Path/GSXXH) became fs:: and an inline
  FNV-1a; the hash only has to notice the DLL changed since the cache was written.

Two changes outside the tree were prerequisites, not cleanups:

- vkCreateDevice now ENABLES vulkanMemoryModel and robustness2's nullDescriptor
  when the device reports them. Both were already probed and never enabled, and a
  shader that declares the memory model on a device where it was not enabled is
  invalid usage rather than a soft fallback -- desktop drivers shrug it off while
  Adreno takes the device down mid-frame, so it would have looked fine until it
  was not. Only nullDescriptor is turned on, not robustness2's expensive half.
- The Android Vulkan loader gained vkFreeDescriptorSets, which it never loaded.

New settings: Frame Generation Target Rate (0 = fixed multiplier) and Frame
Generation Lossless Path. Nothing ships the shaders; they are read from the
user's own legitimately purchased copy, exactly as before.
2026-08-22 03:03:36 -04:00
jpolo1224 d8a98305b8 Library: fail a game the app cannot read, instead of hanging on its loading screen
A game that is visible but unreadable is the failure mode that costs the most
time, because nothing about it looks like a permissions problem. The core opens
games with ordinary file IO. Without All files access the scanner falls back to
SAF document trees, and a title found that way has no filesystem path to open --
but it is still listed, still launches, and then sits on its first loading
screen when the reads never arrive. The same shape appears when the grant is
revoked after a folder was added, or when a disc sits on storage the app was
never given. Moving the same files into the emulator's own games directory fixes
all of it at once, which is what makes the cause so hard to see: the game works
or does not depending only on where it lives.

Boot now proves the content is readable first, at two offsets. Size alone proves
nothing -- an entry can be listed with its real size and still refuse to deliver
bytes -- so it reads byte 0 and byte 32769, the ISO descriptor, the first place
any disc is read for real. Failure returns invalid_file_or_folder with a message
naming the two fixes, rather than handing the core content it cannot read.

The scanner says the same thing at the point it falls back, distinguishing "All
files access is not granted" from "this tree has no filesystem path", because
they need different fixes.

Read-only probe, and directory-installed titles skip it: fs::is_file is false for
them and they were never affected.

Reported by a tester whose whole library started working after moving the ISOs
into ARMSX3/config/games -- cover art, launching, and Prototype no longer locking
up, all from the move alone.
2026-08-21 20:50:04 -04:00
jpolo1224 2f0c63ac1b ISO: stop treating a short magic read as "not an ISO"
Reverts upstream 8a6c96745, taken in the ROP remap merge, back to the 0.8
behaviour. The check is sound on a desktop filesystem, but on Android the disc is
read through SAF/content URIs and this predicate decides how a title gets
mounted: System.cpp:1572, 1589, 1764 and 1878 all branch on it, as does
rpcsx-android.cpp:3935. A short read there does not fail the boot outright, it
silently routes the title down a different mount path -- which is what a game
that reaches its first loading screen and never leaves it looks like.

Not identical to 0.8: magic[] is zero-initialised. The original compared it
without checking the read at all, so a failed read compared uninitialised stack.
Zeroing restores "do not reject on a short read" without the undefined behaviour,
and a genuinely non-ISO file still fails the CD001 test.

iso_archive::is_valid() stays. It comes from a different upstream commit
(d6d5c6082), System.cpp calls it, and it is not part of this predicate.

Reported against NASCAR 2011, which reached the menus before the merge and now
sticks on the first loading screen. NOT confirmed as the cause -- the reporter's
build predates the local commits, and the other public candidates in that window
are the Adreno compute group size and the driver pipeline cache work.
2026-08-21 19:50:32 -04:00
jpolo1224 4b8ffa3702 Release: 0.9.4.1 2026-08-21 17:20:11 -04:00
jpolo1224 c01f2e9ba8 Boot: warn when PPU Threads is not 2
The PS3 has two PPU hardware threads and upstream's own config comment says this
must be 2. With 1, SPURS does not get the concurrent PPU progress it expects and
its task modules fail validation: the SPU executes its own HALT instruction and
the graphics pipeline stops. It presents as an intermittent freeze after 10-30
minutes, on hardware where the same build with the default is fine.

There is no UI for this value -- it is reachable only through a raw core
override, so it is set deliberately and then forgotten. Nothing reported it,
which meant an affected log was indistinguishable from a healthy one and the
setting had to be found by eye in the config dump, after a diagnosis that went
through the SPU halt, the reservation path and an instruction-level SPU test run
before arriving at a configuration difference.
2026-08-21 17:19:44 -04:00
jpolo1224 71c694c9ab Really Slick Screensavers as library backgrounds, and rename the ARMSX2-era settings mirror
Six of Terry Welsh's screensavers (GPL-2.0-or-later) run as animated library
backgrounds alongside Flurry: Flux, Plasma, SolarWinds, Hyperspace, Lattice and
Skyrocket.

They are GL 1.x -- immediate mode, the fixed-function matrix stack, display
lists, texgen -- so savers/gl1.c answers all of that on GLES2 rather than the
renderers being rewritten. Display lists are not an optimisation to skip here:
each saver compiles its particle shape once and replays it per particle with a
different matrix, so glCallList IS how they draw.

Flux, Plasma, SolarWinds and Hyperspace are byte-identical to upstream. They
build with RS_XSCREENSAVER, their own platform-neutral path, and everything it
expects from an X11 host is answered by savers/compat. Lattice and Skyrocket
ship Windows-only and were hand-ported; each carries a header listing exactly
what was removed. rss-glx has neutral versions of those two but is GPL-2.0-only,
which ARMSX2 (GPL-3.0) could not take.

Every saver is compiled inside its own namespace by a *_unit.cpp. They all
declare the same globals -- draw, idleProc, cleanUp, readyToDraw -- because each
was built as a separate executable, and two in one .so collide at link time.

Notes on the three bugs that were not obvious:

- The render thread gets a 16MB stack. Skyrocket's World constructor declares a
  1024x1024x3 starmap as a LOCAL, 3MB, then a 768KB sunsetmap. On a Windows main
  thread that was fine; here it was a SIGSEGV in memset before the first frame.

- gl1_frame_begin clears the first frames and then masks alpha off. These savers
  fade the previous frame instead of clearing, and that fade writes destination
  alpha, dragging a composited surface toward transparent -- which reaches the
  screen as TV static. Same fix Flurry needed.

- The saver lifecycle is serialised behind a mutex and a generation token. Each
  view owns a GL thread, so switching preset let the outgoing thread's teardown
  land after the incoming thread's init and wipe it; gl1 keeps its state in one
  global. A view may now only free the run it started.

Also renames the settings mirror armsx2-settings.json to armsx3-settings.json
(issue #82). The old name stays readable: after a reinstall it is the only copy
of a user's settings, so it is still restored from, still collected into new
backups, and now deleted alongside the new one -- leaving it behind would
silently re-seed settings that had just been purged. Old backup archives restore
unchanged.
2026-08-21 15:14:07 -04:00
jpolo1224 7673186c06 Flurry: preserve the EGL back buffer so the trails can accumulate
The library background rendered as full-screen TV static with the smoke
faintly visible behind it.

Flurry draws its trails by fading the PREVIOUS frame rather than clearing,
so it needs the back buffer to still hold what it drew last time. EGL
defaults EGL_SWAP_BEHAVIOR to EGL_BUFFER_DESTROYED, which leaves the buffer
undefined after every eglSwapBuffers, so every frame started from
uninitialised GPU memory -- which is what reached the screen as static.

Measured: a single fade darkened the buffer by 3.17%, but sixty consecutive
fades only achieved 6.5% in total, where sixty compounding 3.17% fades
should have taken a corner reading 123 down to 17. The fade was working
every frame; its result was not surviving the swap. A 64x64 readback of that
corner went from mean=128 ndiff=41 to mean=0 ndiff=0 with preservation on.

Preservation must be requested on the config and on the surface, and is then
queried back rather than assumed, since a driver may refuse it.

Also set the surface alpha once and mask further alpha writes off. Flurry
fades with GL_SRC_ALPHA/GL_ONE_MINUS_SRC_ALPHA, which writes destination
alpha as well as colour and drags it toward the fade's own value -- harmless
for a screensaver that owns the display, wrong for a surface the window
compositor blends. RGB is untouched, so the trails are unaffected.
2026-08-21 11:49:59 -04:00
jpolo1224 e9a21da302 Library: offer Flurry as the background
Calum Robinson's Flurry screensaver (2002), ported from the xscreensaver tree and
offered alongside the XMB wave. Asked for by a tester who has wanted it on a
handheld since the Mac original.

The renderer is his, unmodified. It draws the way 2002 did -- client-side vertex
arrays, a fixed-function ortho, GL_QUADS -- none of which GLES2 has, so
gl_compat.c answers those calls with a shader, a matrix uniform and an index
buffer instead. That keeps the diff against upstream to four lines: gluBuild2DMipmaps
becomes glTexImage2D plus glGenerateMipmap, GETTIMEOFDAY_TWO_ARGS is set by hand
rather than by xscreensaver's configure, DrawSpark is guarded because it is
immediate-mode and upstream never defines DRAW_SPARKS, and the xlockmore shell is
replaced by four entry points. Its own target rather than part of the JNI glue:
BSD-3-clause next to GPL, and plain C that wants none of the C++20 around it.

FlurryGlView is the same TextureView and EGL shell XmbGlView already uses, so it
reports through onGlStatus the same way and a device that cannot bring GL up
falls back to the 2D backdrop rather than showing a hole.

Off by default, and the description says why. This is a live particle simulation,
and an animated library background has been walked back here once before --
ARMSX2 shipped a looping video behind this same screen and removed it in 2.5.9
when the continuous decode turned out to cost real performance. The loop is
capped at 60fps for the same reason XmbGlView caps itself at 30: Flurry already
refuses to advance faster, but without a cap the thread still spins at panel rate.

Also adds @JvmStatic to Rpcs3Bridge.deleteState, which NativeApp calls from Java.
Kotlin-only compilation does not catch that.
2026-08-21 10:34:27 -04:00
jpolo1224 5d5b8aa4c1 Savestate: add a real delete for slots
Deleting a slot could never work. There was no delete entry point, so the picker
called java.io.File(getGamePathSlot(slot)).delete() -- and getGamePathSlot answers
OCCUPANCY, not a path. Its own declaration two lines below says so: "Not
getGamePathSlot, which answers a title id". So the call was
File("SHVH06660").delete(), which removes nothing and returns false. Every delete
failed, and the UI correctly reported that it had. Issue #80.

Added _rpcsx_deleteStateFromSlot, which resolves the file through
armsx3_slot_find, the same function load uses. That matters: the extension
depends on which build wrote the state -- .zst today, .gz and bare historically --
and building the path by hand is how this went wrong to begin with. The thumbnail
beside it goes too, best effort, so a removed state cannot leave a slot still
showing as occupied.
2026-08-21 09:39:45 -04:00
jpolo1224 a46dae38bc Savestate: do not resume while one is being written
Saving arms after_kill_callback with Emu.Restart and then kills the VM. The
restart runs BootGame, whose restore_on_no_boot does ensure(IsStopped()) -- and
that overload accepts only stopped, loading or stopping. A Resume landing
anywhere in that window puts the state at running and turns the assert into a
process abort.

Issue #81, from the log: "Emulation has been resumed!" at 0:03:23.275950 and
"{Savestate Prepare Thread} Verification failed (object: 0x0)" at 0:03:23.275981.
Thirty one microseconds apart, on different threads.

Android makes the window far easier to hit than desktop. CallFromMainThread runs
its callback INLINE on the calling thread here rather than deferring it, so the
whole kill-and-restart chain executes on the savestate thread while the UI thread
stays free to resume underneath it.

Guarded on m_emu_state_close_pending, which is the emulator's own marker for that
window and is cleared on every failure path, so this cannot leave a game stuck
unresumable. Placed in Resume() rather than in the Android entry point so it also
covers the resume that surface-loss recovery issues.
2026-08-21 09:39:45 -04:00
jpolo1224 b2caae9da2 NP: derive an Ethernet address on Android instead of failing
discover_ether_address() reads the MAC with SIOCGIFHWADDR. Android has not let an
app do that since Android 6 and enforces it hard from 10 -- the ioctl returns
EPERM and /sys/class/net/*/address is unreadable -- so on every modern Android
device that branch failed, np_handler logged

    NPHandler: Failed to discover ethernet or ip address!

and gave up. That leaves the network stack unidentified and blocks RPCN and any
game that asks it who it is. Reported as issue #79 against two unrelated titles,
with network settings that looked correct and could not have helped, because
nothing exposed in them reaches this.

The IP half was never the problem: it learns the local address from a UDP connect
to 8.8.8.8, which is a routing lookup and sends nothing.

Derive the address from Console PSID, exactly as the derive_mac_from_psid path at
the top of the same function already does. Nothing validates it against hardware
-- it identifies the console to the network stack and to peers -- and a locally
administered address is the correct thing to present when the platform refuses
the real one. Console PSID defaults to a per-install random value, so two users
do not end up sharing one.

Tried after the ioctl rather than in place of it, so a device or ROM that does
answer still uses its own.
2026-08-21 09:30:04 -04:00
jpolo1224 0262053a91 Revert "VK: make the fence poll mechanism selectable at runtime"
This reverts commit b91c6551ed.
2026-08-21 01:23:10 -04:00
jpolo1224 b91c6551ed VK: make the fence poll mechanism selectable at runtime
"Is this submission finished" costs 1.9ms per call and returns not-ready 0.0% of
the time on Adreno 740 -- about 5ms a frame in Minecraft, a quarter of the RSX
thread. Measured with the profiler's own counters (fence polls 2.6/frame,
1903712 ns each).

That is the same signature this code already carries a comment about:
vkGetFenceStatus measured 19.7ms per call and never once returned VK_NOT_READY,
"a wait wearing a query's name". The zero-timeout vkWaitForFences that replaced
it is ten times better and still, evidently, a wait -- the driver appears not to
honour a zero timeout either way. That is driver behaviour, so which call is best
is a per-device question and not something to decide here.

ARMSX3_FENCE_POLL in driver_env.txt selects between them:
  wait0   default, today's zero-timeout wait
  status  vkGetFenceStatus, what this used before
  defer   speculative callers do not ask the driver at all

The third is the interesting one. check_present_status walks the queued frames
looking for ones that have already finished and gives up on the first that has
not, and poke_all discards the result entirely -- neither wants to wait, so
neither should be the thing that does. They now pass allow_block=false. next()
and reset() keep the blocking form, so the command buffer ring cannot be starved
by this and "CB chain has run out of free entries" cannot fire because of it.

Default behaviour is unchanged. This ships as a lever rather than a fix on
purpose: the last renderer change justified by one game on one device was
reverted in 0.7.1 after wider testing, and the note on that revert asked for
pieces that come back one at a time with testing behind each. The profiler
already prints per-call timing, so each mode can be judged on the device in front
of whoever is testing.
2026-08-21 01:19:34 -04:00
jpolo1224 72410638ff cellAudio: let the untouched baseline come back down, but not on a flicker
89c6d08ed made untouched_expected a high-water mark to stop a silent port
resetting it every period. That fixed a measured, severe bug -- H.A.W.X. 2 ran
its audio clock at 55% of real time with the ring buffer permanently empty -- and
its own commit message flagged what was left unverified: whether anything depends
on the baseline falling again within a stable port configuration.

It does, and the mark latches. The fast path runs whenever untouched ==
active_ports, and stores min(max(untouched, expected), active_ports), so the first
fully silent period a title has pins the baseline at its maximum. From then on
`untouched > untouched_expected` can never be true and the loop stops waiting for
late ports for the rest of the session. Every game has a silent period.

Going back to upstream's instantaneous store is not the fix: that is exactly what
produced the H.A.W.X. stall. A port a game leaves started while writing only
zeros still flips the -0.0f tags, so it reads as touched on the few periods a
write lands in and untouched on the rest; following that dip drops the baseline
and the next period waits out the whole timeout, every flicker.

Hysteresis covers both, because the two differ in duration rather than shape. The
flicker is one period wide; a game that genuinely starts filling its ports stays
filled. Raise at once, lower only after the lower count has held for eight
periods -- far longer than any flicker, still a small fraction of the untouched
timeouts this feeds. For the H.A.W.X. case the behaviour is unchanged.

Bounded either way: the in_progress gate below is untouched, so no half-written
buffer was ever mixed by this. What the latch cost was the wait for a port that
had not started yet.
2026-08-21 00:57:55 -04:00
jpolo1224 0a3fcc622f Audio: stop the Oboe backend failing silently and permanently
Issue #73: F1 Championship Edition silent since v0.8 except during videos, on
every audio backend. The reporter also notes the races run at 10fps and the
videos are "a bit choppy" -- so audio dies when the machine is saturated and
survives when it is not. That is a load correlation, not a code-path split, and
it rules out the theory that cellAudio is never reached.

cellAudio.cpp has no commits at all in the v0.6..v0.8 window. Across every audio
setting the Android UI writes, the only delta is the default backend moving from
Cubeb to Oboe, with a migration that moved existing users too.

Cubeb starts the OS stream in Open() and never stops it while open; its Play and
Pause only flip a bool. Oboe's call requestStart and requestPause for real, and
this backend asked for an Exclusive low-latency stream with a two-burst buffer --
4 to 10ms. An Exclusive AAudio stream holds the app to the callback deadline and
the platform disconnects it when that is missed for long enough, which at 10fps
is continuous. ARMSX2's own Oboe backend, same team and same device class, uses
Shared with a 4096-frame capacity.

Three fixes here:

Open() asks for Shared, and sizes the buffer from the emulator's own Desired
Audio Buffer Duration rather than a burst multiple. cellAudio fills ahead by that
much, so a stream that cannot hold it underruns however the trigger is set.

Play() started the stream AFTER committing m_playing, and never rolled it back on
failure. onAudioReady requires m_playing but a stream that never started does not
call it, so cellAudio's backend_active never armed, every block it enqueued was
discarded by the !backend_active check in enqueue(), and the early return at the
top of Play() made that permanent for the session. Start first, claim second.

Operational() answered `m_stream && !m_reset_req`, which is true for a stream that
failed to start or was disconnected -- so cellAudio's "backend stopped
unexpectedly" recovery never ran. It now reports the stream state.

Also: audio::configure_audio() is what turns a changed audio node into a rebuilt
backend, and its only caller is main_application.cpp, which is Qt and not part of
this build. Writing Audio Renderer changed the YAML and nothing else until the
next boot, so "try another backend" was a null experiment -- which is how that
issue collected three backends' worth of identical results. Call it from the
Android settings path, alongside the overlay resets that had the same gap.

And the "Audio Backend (advanced)" row only affects Cubeb, since cubeb_backend is
read once in CubebBackend's constructor. Under Oboe it did nothing while staying
interactive and showing a selection. Hidden unless the renderer is Cubeb.
2026-08-21 00:54:16 -04:00
jpolo1224 43df8b0dc2 Android: say why an ARMv8.0 device cannot run, instead of dying on SIGILL
The core is built for armv8.1-a and that is a floor, not a preference:
util/simd.hpp emits SQRDMLAH and util/asm.hpp carries inline LSE atomics. On
ARMv8.0 silicon -- Cortex-A53/A57/A73, so Exynos 9610, Snapdragon 660 and the
like -- those are illegal opcodes.

What the user saw was a bare crash. The fault is a SIGILL inside a static
constructor while the linker is still running libarmsx3-core.so's initialisers,
so it happens before any of our code can report anything, and the top frame names
a log-channel registration rather than a CPU problem. Because the core is
dlopen'd lazily, it surfaced at whatever first needed it -- issue #15 filed it as
"crashes when selecting the firmware .pup file", which is where the crash appears
and not what it is about.

Check /proc/cpuinfo for atomics and asimdrdm before that dlopen and report it.
Checked across every core listed, not just the first, since emulator threads are
scheduled on all of them.

Fails OPEN by design: an unreadable or unfamiliar /proc/cpuinfo returns null and
loads as before. Blocking a device that would have worked is worse than the crash
this replaces. Verified on a Snapdragon 8 Gen 2 that all eight cores report both
flags, so nothing changes there.

The reason is also held in unsupportedCpuMessage so a screen can show it later; a
toast is easy to miss for something this final.
2026-08-21 00:39:58 -04:00
jpolo1224 a14670c282 Config database: add ARMSX3-local per-title overrides, starting with issue #77
The RPCS3 config database is maintained against desktop, so a fix specific to
this port has nowhere upstream to live. Add a small local map layered on top of
whatever the database says.

First entry: Tales of Symphonia Chronicles (BLUS31213, BLES01935) gets Frame
limit "PS3 Native". That is the only mode which honours the game's own
cellGcmSetFlipMode(CELL_GCM_DISPLAY_VSYNC) request -- every other mode flips
immediately, so a title pacing itself by flipping on alternate vblanks free-runs
to the 60 cap, which is issue #77: 60fps in a 30fps game and battles at double
speed.

Applied in three places because none of them alone is enough: after refresh(),
which clears the directory before writing; after setEnabled(), which moves the
whole directory aside; and at startup, so it works for users who never downloaded
the database at all. Merged rather than overwritten, and skipped entirely if the
database already sets a Frame limit for that title.

UNVERIFIED against the reporter's device. The mechanism is read from the code,
not measured, and the failure mode matters: PS3 Native applies NO limit when a
game does not request vsync, so if this title turns out not to, it would run
unbounded rather than capped at 60. Drop the serials from the map if the reporter
says it is worse.
2026-08-21 00:37:39 -04:00
jpolo1224 8c1952deae Frame limit: offer PS3 Native from the FPS cap row
PS3 Native is the only Frame limit mode that honours the game's own
cellGcmSetFlipMode(CELL_GCM_DISPLAY_VSYNC) request: in handle_emu_flip every
other mode falls through and flips immediately, while that one defers the flip to
a real vblank. A title that paces itself to 30fps by flipping on alternate
vblanks therefore free-runs to the 60 cap under Auto, our default -- reported in
issue #77 as Tales of Symphonia running at 60 and battles playing too fast.

The mode already existed and Rpcs3Settings could already write it, but nothing
reachable from the UI ever did: the only control is the Display FPS Cap row,
which maps to numeric presets or Second Frame Limit. It was reachable solely as a
raw core override. That also explains the reporter's follow-up -- capping by hand
does slow the game, but through the wall-clock limiter, which pushes flips off
the vblank boundary and brings tearing and audio glitches with it.

Added as -1 in that row rather than as a new setting, since it is mutually
exclusive with a numeric cap. Both clamps on the way to the core floored at 0 and
would have quietly turned it into "no cap".

setFrameLimitEnabled needed the same widening. It runs last, so its `cap > 0`
test read -1 as "no explicit rate" and wrote Auto back over PS3 Native -- the
collision its own comment describes, one value along.

Deliberately NOT the default: with no vsync request from the game this mode
applies no limit at all, so titles that never ask for vsync would run unbounded.
2026-08-21 00:34:23 -04:00
jpolo1224 c6a0878a97 RSX: restore two hunks lost resolving the ROP remap merge conflict
Both sat inside set_transform_program, the handler upstream rewrote for
ROP_OUTPUT_REMAP. Resolving that conflict took upstream's version as the base and
re-applied our profiler scopes, which put the scopes back and quietly dropped
these.

The first is the one that matters. e13fc184f made the redundant vertex program
check actually compare: the destination holds each word byte-swapped individually
by copy_data_swap_u32, while be_t<u64> swaps all eight bytes and so also exchanges
the two words. Without the rotl the check compares (w0,w1) against (w1,w0), which
can only match when w0 == w1 -- so it never fires, and every single upload marks
the ucode dirty. That forces a vertex program re-analysis, a program cache hint
drop and a full transform constant re-upload on every draw.

That is a per-draw cost on the RSX thread, restored to 0.9.4 by the merge after
being fixed, and it fits issue #78: Wipeout HD Fury reported as much higher CPU
and far worse performance on 0.9.4 than 0.9.3, with no other change in the window
that adds per-draw work.

The second is g_xform_program_words, which the profiler divides by
g_xform_program_calls to report average batch size. Losing the increment prints 0
rather than printing nothing -- exactly the failure mode I checked for on the
transform CONSTANT counter during the same merge, and missed on this one.

ISO.cpp, the other conflicted file, was checked the same way and lost nothing.
2026-08-21 00:23:37 -04:00
jpolo1224 b9689d07fd VK: allow the driver pipeline cache to be switched off at runtime
ARMSX3_PIPELINE_CACHE=0, read through driver_env.txt like the compute group size
override, so a suspected regression can be A/B'd on a single build.

Issue #78 reports Wipeout HD Fury running far worse on 0.9.4 than 0.9.3 with much
higher CPU. That window holds 28 commits, and most of them only remove work, so
the plausible suspects are this cache, the upstream ROP output remap, and the ISO
reader revert. Guessing between them is not worth a release each.

This one is worth being able to eliminate first because it is the only change that
introduces a structure shared between threads that previously shared none: up to
eight shader cache loader workers at boot, plus the async pipeline compiler
workers during play, all now insert into one VkPipelineCache. Concurrent use is
permitted by the spec, but the driver still has to serialise its own inserts.

Not evidence that it IS the cause -- it is the cheapest candidate to rule out.
2026-08-21 00:14:00 -04:00
jpolo1224 82f21b16d2 VK: credit sashkinbro for the pipeline cache format and the Adreno split
Both landed as our own commits and both owe him more than they said.

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

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

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

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

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

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

    m_state <= system_state::stopping

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two conflicts.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Three things this deliberately does not do:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Above that, three dead links in a row.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

That is not hypothetical. A test device carried

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two things fall out of the same change:

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

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

Booting a game logged

  canary patches: imported 1, enabled 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Measured after (same scene and script, healthy device): sync() falls to
0.10% of the RSX thread's wall time, the thread parks in the kernel for
67.6% of the workload, and fps is unchanged within run noise (52.9 avg
vs 51.4 for the pre-review variant in the same session). The win is a
freed core and its thermal budget, not frame rate. The
non-RSX-thread branch has the same spin shape; it was not measured and
is left untouched.
2026-08-15 06:47:59 -05:00
digant73 fc93d932c8 Swap PR number with PR text in update manager 2026-08-15 13:28:58 +02:00
Diego BM 89b2128679 Update EmulationMenuScreen.kt
Lingering Playstation 2 naming on the side menu during execution
2026-08-15 13:14:33 +02:00
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
421 changed files with 135926 additions and 7800 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);
+378 -9
View File
@@ -2,6 +2,7 @@
#include "Emu/System.h"
#include "Emu/Cell/SPUThread.h"
#include "Emu/Cell/PPUThread.h"
#include "Emu/Cell/PPUDisAsm.h"
#include "Emu/Cell/lv2/sys_mmapper.h"
#include "Emu/Cell/lv2/sys_event.h"
#include "Emu/Cell/lv2/sys_process.h"
@@ -23,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>
@@ -63,6 +69,12 @@ DYNAMIC_IMPORT_RENAME("Kernel32.dll", SetThreadDescriptionImport, "SetThreadDesc
#include <sys/timerfd.h>
#include <unistd.h>
#endif
#ifdef __ANDROID__
// For the allocation-free breadcrumb the fault handler writes before it risks anything else, and
// for reaching libsigchain's registration entry point without linking against the ART apex.
#include <android/log.h>
#include <dlfcn.h>
#endif
#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
# include <sys/sysctl.h>
@@ -2216,6 +2228,63 @@ bool handle_access_violation(u32 addr, bool is_writing, bool is_exec, ucontext_t
else
{
vm_log.always()("[%s] Access violation %s location 0x%x (%s)", cpu->get_name(), is_writing ? "writing" : "reading", addr, (is_writing && vm::check_addr(addr)) ? "read-only memory" : "unmapped memory");
// The guest code at the fault AND at its callers.
//
// Registers and a call stack come free from dump_useful_thread_info() above,
// and for a bad pointer they are only half the answer: they say WHAT the
// address was, never what computed it. When the faulting function turns out to
// be something generic -- Borderlands 2 faults inside a memcpy, handed
// dest=0x93aef33d and length=0xc3aaf87d, both garbage -- the routine itself is
// blameless and the whole question is which caller filled those arguments.
//
// So: a window at cia, then one at each of the first few return addresses. Only
// a few, because a PPU call stack here runs fourteen frames deep and the answer
// is almost always in the immediate caller.
//
// Every address is checked before it is read: cia and the stack are taken from
// a thread that just faulted, so both can be garbage, and faulting inside the
// diagnostic that explains a fault would be the worst possible trade.
if (cpu->get_class() == thread_class::ppu)
{
PPUDisAsm dis_asm(cpu_disasm_mode::dump, vm::g_sudo_addr);
std::string code;
const auto window = [&](const char* what, u32 pc, u32 back, u32 span)
{
fmt::append(code, "\n%s 0x%08x:\n", what, pc);
for (u32 at = pc >= back ? pc - back : 0; at <= pc + span; at += 4)
{
if (!vm::check_addr(at))
{
continue;
}
dis_asm.disasm(at);
code += (at == pc ? " >>" : " ");
code += dis_asm.last_opcode;
}
};
window("Code at the faulting pc", static_cast<ppu_thread*>(cpu)->cia, 0x40, 0x40);
u32 shown = 0;
for (auto&& [ret, sp] : cpu->dump_callstack_list())
{
if (shown++ >= 3)
{
break;
}
// Back further than forward: the call is BEHIND the return address,
// and what fills the arguments sits behind that.
window("Code at caller", ret, 0x60, 0x10);
}
vm_log.always()("Guest code around the fault:%s", code);
}
}
}
@@ -2253,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())
@@ -2549,10 +2641,165 @@ const bool s_exception_handler_set = []() -> bool
#else
static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
#ifdef __ANDROID__
// The handlers that were installed before ours -- libsigchain's, which fronts ART and debuggerd.
// Kept so that faults which are not the emulator's can be forwarded to them.
static struct ::sigaction s_prev_fault_action[NSIG]{};
// True when this fault is one the emulator's own memory model is responsible for.
//
// The ranges are exactly the ones the handler can act on: guest memory (try_get_addr spans 8GiB
// from g_base_addr, so the sudo mirror is included), the executable map, and the segment map.
// Everything else is somebody else's fault, in both senses.
static bool is_emulator_fault(void* addr)
{
const u64 exec64 = (reinterpret_cast<u64>(addr) - reinterpret_cast<u64>(vm::g_exec_addr)) / 2;
const u64 seg_off = (reinterpret_cast<u64>(addr) - reinterpret_cast<u64>(vm::g_exec_addr)) - vm::g_exec_addr_seg_offset;
return vm::try_get_addr(addr).second || exec64 < 0x100000000ull || seg_off < 0x80000000ull;
}
// Bionic's own sigaction, reached past libsigchain's interposition.
//
// libsigchain exports sigaction() and is loaded with global visibility, so an ordinary call
// registers us INSIDE ART's chain -- behind its FaultManager, which is the entire problem. Looking
// the symbol up in libc's own handle gets the real one, letting us install at the kernel level and
// genuinely go first. RTLD_NOLOAD because libc is obviously already here; this must never load
// anything. Returns null if bionic ever stops exporting it, and the caller then keeps the ordinary
// registration rather than starting with no handler at all.
using armsx3_sigaction_fn = int (*)(int, const struct ::sigaction*, struct ::sigaction*);
static armsx3_sigaction_fn real_sigaction()
{
void* const libc = ::dlopen("libc.so", RTLD_NOLOAD | RTLD_LOCAL);
return libc ? reinterpret_cast<armsx3_sigaction_fn>(::dlsym(libc, "sigaction")) : nullptr;
}
// Installs a fault handler, remembering what it replaced.
static int install_fault_handler(int sig, const struct ::sigaction& sa)
{
return ::sigaction(sig, &sa, sig > 0 && sig < NSIG ? &s_prev_fault_action[sig] : nullptr);
}
#else
static int install_fault_handler(int sig, const struct ::sigaction& sa)
{
return ::sigaction(sig, &sa, nullptr);
}
#endif
// Installs a fault handler ahead of the Android runtime, not inside its chain.
//
// Two registrations were a mistake worth recording. Registering through the interposed sigaction()
// AS WELL as at kernel level puts this handler in libsigchain's chain, so forwarding a fault that
// is not ours goes to libsigchain, which walks its chain straight back to here, which forwards
// again -- recursing until the alternate stack is gone. The process died of that with no
// breadcrumb, no tombstone and no ART frames: quieter than the bug it was meant to fix.
//
// So: capture the handler the kernel currently calls (libsigchain's, which fronts ART), then
// replace it, and never register through the interposed entry point for this signal. The chain we
// forward into then does not contain us.
static bool install_fault_handler_first(int sig, const struct ::sigaction& sa)
{
#ifdef __ANDROID__
if (const armsx3_sigaction_fn real_sa = real_sigaction())
{
if (real_sa(sig, nullptr, &s_prev_fault_action[sig]) != -1 && real_sa(sig, &sa, nullptr) != -1)
{
char line[96];
if (::snprintf(line, sizeof(line), "sigchain: installed ahead of the runtime for signal %d", sig) > 0)
{
__android_log_write(ANDROID_LOG_INFO, "ARMSX3", line);
}
return true;
}
}
__android_log_write(ANDROID_LOG_WARN, "ARMSX3", "sigchain: could not get ahead of the runtime; it will see faults first");
#endif
// No bionic entry point, or it refused: fall back to the ordinary registration. The runtime
// then sees faults first, which is how this behaved before, crash included.
return install_fault_handler(sig, sa) != -1;
}
// What a SIGBUS was actually about. si_code is the only thing that tells an unbacked page apart
// from a misaligned operand, and those point at completely different bugs.
static const char* bus_error_kind(int code) noexcept
{
switch (code)
{
case BUS_ADRALN: return "misaligned operand";
case BUS_ADRERR: return "mapped page has no backing";
case BUS_OBJERR: return "hardware error on the mapped object";
default: return "unrecognised si_code";
}
}
static void signal_handler(int sig, siginfo_t* info, void* uct) noexcept
{
ucontext_t* context = static_cast<ucontext_t*>(uct);
#ifdef __ANDROID__
// Not our fault: hand it to whoever we displaced.
//
// We install ahead of libsigchain deliberately (see the registration site), which means ART's
// FaultManager no longer sees the emulator's own faults -- it was reading guest registers as
// ArtMethod* and dying. But ART still needs its own faults: implicit null checks in JIT'd Java
// code arrive as SIGSEGV and are how a NullPointerException gets thrown. This forward is what
// keeps that working, and keeps ordinary tombstones for crashes that are genuinely elsewhere.
if (!is_emulator_fault(info->si_addr))
{
// Forward once and once only. If whatever we forward to comes back here -- which it did
// while this handler was also registered inside libsigchain's chain -- looping would burn
// the alternate stack and kill the process silently. Second time through, stand down: put
// the default action back and return, so the instruction faults again and the platform
// produces an honest tombstone instead of a recursion.
static thread_local bool s_forwarding = false;
if (s_forwarding)
{
struct ::sigaction dfl{};
dfl.sa_handler = SIG_DFL;
sigemptyset(&dfl.sa_mask);
::sigaction(sig, &dfl, nullptr);
return;
}
const struct ::sigaction& prev = s_prev_fault_action[sig];
s_forwarding = true;
if ((prev.sa_flags & SA_SIGINFO) && prev.sa_sigaction)
{
prev.sa_sigaction(sig, info, uct);
s_forwarding = false;
return;
}
if (prev.sa_handler && prev.sa_handler != SIG_DFL && prev.sa_handler != SIG_IGN)
{
prev.sa_handler(sig);
s_forwarding = false;
return;
}
s_forwarding = false;
}
#endif
// SIGBUS arrives here too now (see the sigaction block below), and never takes a recovery
// path. The recovery below is for pages this process protected itself, and a write to an
// mprotect'd page raises SIGSEGV/SEGV_ACCERR, never SIGBUS. A bus error means the page behind
// an otherwise valid address could not be produced at all -- unbacked, past the backing size,
// or an operand the instruction cannot address at that alignment. Nothing here changes any of
// those, so handling one and returning would re-execute the same instruction and fault again
// immediately: a livelock in place of a crash report.
const bool is_bus_error = sig == SIGBUS;
#if defined(ARCH_X64)
#ifdef __APPLE__
const u64 err = context->uc_mcontext->__es.__err;
@@ -2635,7 +2882,10 @@ static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
const u64 seg_off = (reinterpret_cast<u64>(info->si_addr) - reinterpret_cast<u64>(vm::g_exec_addr)) - vm::g_exec_addr_seg_offset;
const auto cause = is_executing ? "executing" : is_writing ? "writing" : "reading";
if (auto [addr, ok] = vm::try_get_addr(info->si_addr); ok && !is_executing)
// Gated on more than "not an instruction fetch" now: see is_bus_error above.
const bool try_recovery = !is_executing && !is_bus_error;
if (auto [addr, ok] = vm::try_get_addr(info->si_addr); ok && try_recovery)
{
// Try to process access violation
if (thread_ctrl::get_current() && handle_access_violation(addr, is_writing, false, context))
@@ -2644,14 +2894,14 @@ static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
}
}
if (exec64 < 0x100000000ull && !is_executing)
if (exec64 < 0x100000000ull && try_recovery)
{
if (thread_ctrl::get_current() && handle_access_violation(static_cast<u32>(exec64), is_writing, true, context))
{
return;
}
}
else if (seg_off < 0x80000000ull && !is_executing)
else if (seg_off < 0x80000000ull && try_recovery)
{
if (thread_ctrl::get_current() && handle_access_violation(static_cast<u32>(seg_off * 2), is_writing, true, context))
{
@@ -2659,7 +2909,92 @@ static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
}
}
std::string msg = fmt::format("Segfault %s location %p at %p.\n", cause, info->si_addr, RIP(context));
#ifdef __ANDROID__
// Raw state, before anything that can fault.
//
// Placed here deliberately: every recovery path above has already declined, so this only runs
// for faults that are actually fatal -- the write-protection faults the RSX relies on come
// through here hundreds of times a second and must not be logged at all.
//
// Everything below this point formats strings, allocates, takes the logger's locks and walks
// thread and guest state, and on a process sick enough to be here any of those can fault
// again. A second fault while this signal is blocked is force-delivered with the default
// action, killing the process instantly with the fatal message still sitting unflushed in the
// async log -- Borderlands 2 died that way three times, handler reached, nothing written.
//
// So: fixed stack buffers and liblog writes. No allocation, no locks, no ordering with the
// async log. Registers and the faulting instruction are what a wild address needs anyway --
// they say which operand went bad, which the formatted report never does.
{
char line[256];
const u64 pc = RIP(context);
if (::snprintf(line, sizeof(line), "fatal signal %d (si_code %d) at %p, pc 0x%llx, tid %d",
sig, info->si_code, info->si_addr, static_cast<unsigned long long>(pc),
static_cast<int>(::syscall(__NR_gettid))) > 0)
{
__android_log_write(ANDROID_LOG_FATAL, "ARMSX3", line);
}
#if defined(ARCH_ARM64)
// Only when the fault was a data access: an instruction-fetch fault means pc itself is
// what could not be read, so reading it here would fault a second time.
if (!is_executing && ::snprintf(line, sizeof(line), " insn 0x%08x", *reinterpret_cast<const u32*>(pc)) > 0)
{
__android_log_write(ANDROID_LOG_FATAL, "ARMSX3", line);
}
for (int i = 0; i < 31; i += 4)
{
char* p = line;
int rem = static_cast<int>(sizeof(line));
for (int j = i; j < i + 4 && j < 31; ++j)
{
const int w = ::snprintf(p, rem, " x%d=0x%llx", j, static_cast<unsigned long long>(GPR(context, j)));
if (w <= 0 || w >= rem)
{
break;
}
p += w;
rem -= w;
}
__android_log_write(ANDROID_LOG_FATAL, "ARMSX3", line);
}
#endif
}
// A fault outside guest memory is handed straight back to the platform's crash handler.
//
// Installing this handler displaced debuggerd's, which is why none of these crashes ever
// produced a tombstone: emergency_exit() takes the process down itself, throwing away the one
// artifact carrying a symbolised backtrace of every thread. For a guest access violation that
// is the right trade -- the emulator reports those far better than a tombstone would. For a
// fault at an address that is not guest memory, the backtrace IS the diagnosis: it names
// whoever handed out the corrupt pointer, which nothing here can work out by itself.
//
// Before the formatting below, not after, and this is the whole point: the report allocates,
// and on a process whose heap is already corrupt the allocation faults again. That second
// fault killed the process every time, so a chain placed after the report never ran.
//
// Restoring the previous handler and returning rather than re-raising: the faulting
// instruction executes again and faults again, so debuggerd sees the original pc, address and
// registers instead of this handler's frame. Ours is no longer installed, so there is no loop.
if (!vm::try_get_addr(info->si_addr).second && s_prev_fault_action[sig].sa_sigaction)
{
::sigaction(sig, &s_prev_fault_action[sig], nullptr);
return;
}
#endif
// Named for what it was: a bus error reported as "Segfault" sends whoever reads the log
// looking for a bad pointer, when the address is usually fine and the mapping behind it is not.
std::string msg = sig == SIGBUS
? fmt::format("Bus error (%s) %s location %p at %p.\n", bus_error_kind(info->si_code), cause, info->si_addr, RIP(context))
: fmt::format("Segfault %s location %p at %p.\n", cause, info->si_addr, RIP(context));
if (vm::try_get_addr(info->si_addr).second)
{
@@ -2700,6 +3035,13 @@ static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
#endif
sys_log.fatal("\n%s", msg);
// Flushed here rather than only after the dump. dump_useful_thread_info() walks thread state
// and guest memory, so it is the single most likely thing in this handler to fault again, and
// a fault there loses the fatal message with it -- it is still sitting in the async log's
// buffer at this point. The message is the part worth keeping; the dump is a bonus.
logs::listener::sync_all();
sys_log.notice("\n%s", dump_useful_thread_info());
logs::listener::sync_all();
@@ -2758,14 +3100,24 @@ const bool s_exception_handler_set = []() -> bool
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = signal_handler;
if (::sigaction(SIGSEGV, &sa, NULL) == -1)
if (!install_fault_handler_first(SIGSEGV, sa))
{
std::fprintf(stderr, "sigaction(SIGSEGV) failed (%d).\n", errno);
std::abort();
}
#ifdef __APPLE__
if (::sigaction(SIGBUS, &sa, NULL) == -1)
#if defined(__APPLE__) || defined(__ANDROID__)
// Android too, and not for tidiness: with no handler, SIGBUS takes the default action and
// the process dies having written nothing at all -- no line from this handler, no tombstone,
// and an RPCSX.log that simply stops mid-sentence. The only record of Borderlands 2 dying
// this way was one Zygote line, "exited due to signal 7 (Bus error)".
//
// It is a fault class this emulator can genuinely hit. Guest memory is a MAP_SHARED mapping
// of a memfd, and a shared file mapping raises SIGBUS rather than SIGSEGV whenever the page
// behind an otherwise valid address cannot be produced -- past the backing size, or with
// nothing left to back it. None of that is recoverable here, but all of it is diagnosable,
// and none of it was.
if (!install_fault_handler_first(SIGBUS, sa))
{
std::fprintf(stderr, "sigaction(SIGBUS) failed (%d).\n", errno);
std::abort();
@@ -2773,7 +3125,7 @@ const bool s_exception_handler_set = []() -> bool
#endif
sa.sa_sigaction = sigill_handler;
if (::sigaction(SIGILL, &sa, NULL) == -1)
if (install_fault_handler(SIGILL, sa) == -1)
{
std::fprintf(stderr, "sigaction(SIGILL) failed (%d).\n", errno);
std::abort();
@@ -2855,8 +3207,25 @@ void thread_base::start()
ensure(pthread_create(&thread_id, &attrs, entry_point, this) == 0);
#else
pthread_t thread_id{};
#ifdef __ANDROID__
// Give Android threads the stack desktop Linux already gives them.
//
// bionic's default is 1 MB; glibc's is 8 MB. Passing null attributes here meant every emulator
// thread on Android ran on an eighth of the stack the same code gets everywhere else, and
// nothing said so -- an SPU thread's stack mapping measured 0xfc000.
//
// Address space only; stack pages are committed on first use.
pthread_attr_t attrs;
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 0x800000);
const int rc = pthread_create(&thread_id, &attrs, entry_point, this);
pthread_attr_destroy(&attrs);
ensure(rc == 0);
#else
ensure(pthread_create(&thread_id, nullptr, entry_point, this) == 0);
#endif
#endif
#ifndef _WIN32
// Update m_thread atomically
+29 -9
View File
@@ -1803,13 +1803,6 @@ static void append_patches(patch_engine::patch_map& existing_patches, const patc
bool patch_engine::save_patches(const patch_map& patches, const std::string& path, std::stringstream* log_messages)
{
fs::file file(path, fs::rewrite);
if (!file)
{
append_log_message(log_messages, fmt::format("Failed to open patch file %s (%s)", path, fs::g_tls_error), &patch_log.fatal);
return false;
}
YAML::Emitter out;
out << YAML::BeginMap;
out << patch_key::version << patch_engine_version;
@@ -1904,7 +1897,24 @@ bool patch_engine::save_patches(const patch_map& patches, const std::string& pat
out << YAML::Flow;
out << YAML::BeginSeq;
out << fmt::format("%s", data.type);
out << fmt::format("0x%.8x", data.offset);
// move_file and hide_file carry a VFS path in the address element instead of a
// number. load() keeps that text in original_offset and skips the u32 validation for
// them, so formatting it numerically here would write out 0x00000000 and the loader
// would accept it back as a patch that silently never matches anything.
//
// The numeric branch deliberately uses offset rather than original_offset: an
// address modifier is folded into offset at load time, and the flat form emitted
// here has nowhere to put it.
if (patch_type_uses_hex_offset(data.type))
{
out << fmt::format("0x%.8x", data.offset);
}
else
{
out << data.original_offset;
}
out << data.original_value;
out << YAML::EndSeq;
}
@@ -1918,7 +1928,17 @@ bool patch_engine::save_patches(const patch_map& patches, const std::string& pat
out << YAML::EndMap;
file.write(out.c_str(), out.size());
// Write through a temporary and rename on success, as save_config already does. A truncating
// in-place write that fails part way (out of space, process killed) leaves a half-written file,
// and load() rejects the whole file on a parse error -- so a failure here costs the user every
// patch they had, with no way to rebuild it from inside the app.
fs::pending_file file(path);
if (!file.file || file.file.write(out.c_str(), out.size()) < out.size() || !file.commit())
{
append_log_message(log_messages, fmt::format("Failed to write patch file %s (%s)", path, fs::g_tls_error), &patch_log.fatal);
return false;
}
return true;
}
+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
+106 -12
View File
@@ -1,3 +1,5 @@
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
@@ -32,19 +34,21 @@ 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 = 14
versionName = "0.8"
versionCode = 23
versionName = "0.9.4.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.
//
// On because ARMSX3 ships as a sideloaded APK from its own GitHub releases, which is
// exactly the case an in-app updater is for. It must go back off, and the code and the
// REQUEST_INSTALL_PACKAGES permission must move into a github-only flavor, before any
// Play build exists: Play forbids self-updating apps, and it is the PERMISSION in the
// bundle that gets rejected, which this runtime flag does nothing about.
// These are the github values; the play flavor overrides all three below.
//
// The warning that used to live here was right and is now acted on: a runtime boolean
// does nothing about the PERMISSION in the bundle, which is what Play rejects. The
// permissions have moved into the github flavor's manifest, so the play bundle does not
// declare them at all.
buildConfigField("boolean", "STORAGE_ALL_FILES", "true")
buildConfigField("boolean", "IN_APP_UPDATER", "true")
buildConfigField("boolean", "FRAME_GENERATION", "true")
ndk {
// The core is arm64-only.
@@ -68,6 +72,40 @@ android {
}
}
// Two distributions, and they are not interchangeable.
//
// github is the sideloaded build: it updates itself from GitHub releases, can be pointed at
// an arbitrary data folder, and ships frame generation.
//
// play is what Google Play will accept. Self-updating is forbidden outright, all-files
// storage is a policy review it does not need, and frame generation is left out. The
// applicationId differs so the two install side by side instead of over each other.
flavorDimensions += "distribution"
productFlavors {
create("github") {
dimension = "distribution"
}
create("play") {
dimension = "distribution"
applicationId = "com.armsx3.play"
buildConfigField("boolean", "STORAGE_ALL_FILES", "false")
buildConfigField("boolean", "IN_APP_UPDATER", "false")
buildConfigField("boolean", "FRAME_GENERATION", "false")
// Frame generation is excluded by SOURCE SET, not by a packaging filter: a
// packaging block inside a flavor is not honoured and silently applied to both,
// which dropped the library from the github build too. libarmsx3_lsfg.so lives in
// src/github/jniLibs, so only that flavor bundles it.
//
// Excluding the file is the whole exclusion. The shim is dlopen'd by name, and the
// core already reports frame generation unavailable when the library is absent,
// which is the same path a device that cannot run it takes.
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
@@ -75,17 +113,73 @@ android {
}
}
// Reads android/armsx3-ui/keystore.properties when it exists:
//
// storeFile=/absolute/path/to/upload.jks
// storePassword=...
// keyAlias=upload
// keyPassword=...
//
// Absent, only the debug key exists and release builds stay sideload-only. The file is
// gitignored and nothing here echoes its contents.
signingConfigs {
val props = rootProject.file("keystore.properties")
if (props.exists()) {
val k = Properties().apply { props.inputStream().use { load(it) } }
create("upload") {
storeFile = file(k.getProperty("storeFile"))
storePassword = k.getProperty("storePassword")
keyAlias = k.getProperty("keyAlias")
keyPassword = k.getProperty("keyPassword")
}
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
// Off for the Play bundle, on for GitHub APKs.
//
// Not a preference: AGP 9.2.1's R8 writes its mapping as mapping.prt, a compressed
// per-class archive, while packageBundle still demands a plain mapping.txt, so an
// AAB cannot be built with R8 enabled at all. Set by build-play-aab.sh.
//
// The cost is small and there is precedent: ARMSX2 ships its Play build with minify
// off entirely, and here a 94 MB native core dominates a 76 MB APK, so shrinking the
// Kotlin saves comparatively little.
//
// A gradle property rather than the variant API, matching how armsx3.minSdk is
// already threaded through by build-variants.sh.
val noMinify = project.hasProperty("armsx3.noMinify")
isMinifyEnabled = !noMinify
isShrinkResources = !noMinify
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// Debug-signed so alpha release builds are sideloadable without the
// upload key. Swap this for the real config before any public build.
signingConfig = signingConfigs.getByName("debug")
// The upload key when one is configured, the debug key otherwise.
//
// GitHub APKs are deliberately debug-signed so an alpha stays sideloadable without
// the upload key present. Play rejects a debug-signed bundle outright, so
// build-play-aab.sh refuses to run without keystore.properties.
//
// The file is gitignored (*.jks, keystore.properties) and read at build time, so no
// credential is ever in the repo or on a command line.
// The upload key ONLY when explicitly asked for, which build-play-aab.sh does.
//
// Opt-in rather than "use it if it exists": once the keystore was created, every
// release build silently started using it, and a differently-signed APK cannot be
// installed over an existing one. That turns a sideload build into something testers
// cannot install, and the error Android shows says nothing about signatures. It was
// being worked around by hiding keystore.properties by hand before each build, which
// is exactly the kind of step that gets forgotten once.
signingConfig = if (project.hasProperty("armsx3.uploadSigning")) {
signingConfigs.findByName("upload")
?: throw GradleException("armsx3.uploadSigning set but keystore.properties is missing")
} else {
signingConfigs.getByName("debug")
}
}
}
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Everything Google Play will not accept, declared by the github flavor alone so the play
bundle cannot inherit it by accident.
REQUEST_INSTALL_PACKAGES and the FileProvider are what the in-app updater needs: it downloads
a GitHub release APK and hands it to the system package installer. Play forbids apps that
update themselves outside the store, and it is the DECLARED PERMISSION that gets rejected, so
gating the code behind a runtime flag was never enough on its own.
MANAGE_EXTERNAL_STORAGE backs the all-files onboarding path, which lets the data folder live
anywhere on the device. The play build uses the app-specific directories instead (internal or
SD card), which are raw-writable under scoped storage with no permission at all.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application>
<!-- Hands the downloaded update APK to the system package installer. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.updateprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_paths" />
</provider>
</application>
</manifest>
@@ -23,16 +23,10 @@
Sideload/GitHub builds only. The Play flavour must NOT ship this (the
policy needs a declared exemption); that is what the STORAGE_ALL_FILES
buildConfig flag gates in code. -->
<!-- In-app updater: install the downloaded APK. SIDELOAD ONLY.
A self-updating app is a hard Play-policy violation, and it is this permission in the
bundle that gets rejected, not the runtime flag. ARMSX2 keeps it out of its Play build
with a github-only flavor and a build script that fails closed if it ever appears;
ARMSX3 has no Play build, so it lives here. Adding a Play target means moving this and
the provider below into a github flavor FIRST. -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!-- REQUEST_INSTALL_PACKAGES, MANAGE_EXTERNAL_STORAGE and the updater's FileProvider are
declared by the GITHUB flavor only, in src/github/AndroidManifest.xml. Play rejects the
permission present in the bundle, not the code path behind a runtime flag, so none of
them may sit here where both flavors inherit them. -->
<!-- Optional motion controls (Pad settings). Not required, so the Play install
isn't gated for devices without a gyroscope. -->
@@ -204,18 +198,6 @@
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:excludeFromRecents="true"
android:exported="false" />
<!-- Hands the downloaded update APK to the system package installer. Paired with
REQUEST_INSTALL_PACKAGES above; see the note there before shipping to Play. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.updateprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_paths" />
</provider>
</application>
@@ -50,3 +50,44 @@ PPU-4b46d0161ca657ab16b0a779d9062810ea5ea2dd:
- [ jumpf, 0x00000000, "RPCS3_HLE_LIBRARY:WaitForSPUsToEmptySNRs" ] # Args: (SPU ID, 3)
- [ be32, 0x00000000, 0x38800000 ] # li r4, 0
- [ be32, 0x00000000, 0x44000002 ] # sc
# Tom Clancy's H.A.W.X. 2 (BLES00928) -- boot hang at the first intro video.
#
# The SPU dies with "Access violation reading location 0x20" in CellSpursKernel0 and
# is parked forever (dbg_pause, which nothing in the Android build can clear), so the
# emulator looks healthy at a locked 30 fps while the guest is dead. Upstream RPCS3
# lists the title as Loadable with no fix but "delete data/movies".
#
# The title looks up a section named '.reload' in this SPU module. The module is
# stripped -- e_shnum = 0 -- so the lookup can never succeed, on hardware either, and
# the game is built to cope: the failure path writes 0 to the work descriptor's +0x10
# field, and this very module tests that field to skip the overlay load:
#
# 03224 lqr r8,0x1b810 ; r8 = desc[+0x10]
# 0322c brz r8,0x32cc ; == 0 -> skip
#
# A bump allocator on the PPU side then runs over the field unconditionally --
# (0 - 0x10) & ~0xF = 0xfffffff0 -- destroying the sentinel. The guard no longer
# fires, so the SPU issues GET lsa=0 ea=0 size=0x4000, a transfer that would have
# overwritten the running SPURS kernel had it succeeded.
#
# This makes the overlay routine at LS 0x3208 return immediately, which is what the
# surviving guard would have caused anyway. Safe because the section it needs cannot
# exist in a stripped module. Suppressing the DMA instead does NOT work: the guest
# loop waits on data that never arrives and runs away.
#
# Ps3PatchRepo.BUNDLED must list this, or it is imported but never enabled.
SPU-42bae8e5d6a9304068ba1c6bbfdc18d656e287a1:
Bink overlay skip:
Games:
"Tom Clancy's H.A.W.X. 2":
BLES00928:
- "All"
Author: Zulux91
Patch Version: 1.0
Notes: Fixes the boot hang at the first intro video.
Patch:
# LS 0x3208 is the first instruction of the overlay routine (il r5,0).
# Offsets are LS addresses: apply_modification subtracts p_vaddr (0x3000).
- [ be32, 0x3208, 0x35000000 ] # bi lr -- return immediately
@@ -23,6 +23,131 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
adrenotools
)
# ---------------------------------------------------------------------------
# Flurry -> libflurry.so
#
# Calum Robinson's Flurry screensaver (2002), offered as a live library background. Its own
# target rather than folded into the glue for two reasons: it is BSD-3-clause next to GPL code
# and the boundary should be visible, and it is plain C from 2002 that wants none of the C++20
# and adrenotools the glue is built with.
#
# gl_compat.c answers the GL 1.x calls the sources make -- client-side vertex arrays, a
# fixed-function ortho and GL_QUADS -- with a GLES2 shader, so the renderer itself stays
# unmodified. See its header for what is and is not emulated.
# ---------------------------------------------------------------------------
add_library(flurry SHARED
flurry/flurry_jni.c
flurry/gl_compat.c
flurry/flurry.c
flurry/flurry-smoke.c
flurry/flurry-spark.c
flurry/flurry-star.c
flurry/flurry-texture.c
)
target_include_directories(flurry PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/flurry)
set_target_properties(flurry PROPERTIES C_STANDARD 99)
# -ffast-math: a particle simulation judged by eye, running thousands of sqrt per frame.
# Upstream shipped hand-written frsqrte paths for exactly this reason.
target_compile_options(flurry PRIVATE -O2 -ffast-math -Wall -Wno-unused-parameter)
target_link_libraries(flurry log GLESv2 m)
# ---------------------------------------------------------------------------
# Really Slick Screensavers -> libsavers.so
#
# Terry Welsh's savers (GPL-2.0-or-later), compiled against a GLES2 shim rather than rewritten.
# The saver sources are BYTE-IDENTICAL to upstream: they are built with RS_XSCREENSAVER, which
# selects their platform-neutral path, and everything that path expects is answered by
# compat/. That keeps them re-pullable and keeps them recognisably his code.
#
# Separate from the flurry target because these are C++ and Flurry is C99, and because Flurry's
# shim is a different, smaller one. If a saver ever needs Flurry's, the two can merge.
add_library(savers SHARED
savers/savers_jni.cpp
savers/savers_platform.cpp
savers/gl1.c
# Each saver is compiled through a *_unit.cpp that wraps it in a namespace: they all
# declare the same globals (draw, idleProc, cleanUp, readyToDraw) because each was built
# as its own executable, and two in one .so collide at link time.
savers/flux/flux_unit.cpp
savers/flux/flux_port.cpp
savers/plasma/plasma_unit.cpp
savers/plasma/plasma_port.cpp
savers/solarwinds/solarwinds_unit.cpp
savers/solarwinds/solarwinds_port.cpp
# Hyperspace. extensions.cpp is deliberately NOT built: it is the WIN32/GLX loader for the
# ARB shader path, and the port takes the non-shader path instead. The entry points it
# would have resolved are declared in compat/arb_shaders.h and defined in savers_platform.
savers/hyperspace/unit_causticTextures.cpp
savers/hyperspace/unit_flare.cpp
savers/hyperspace/unit_goo.cpp
savers/hyperspace/unit_hyperspace.cpp
savers/hyperspace/unit_splinePath.cpp
savers/hyperspace/unit_starBurst.cpp
savers/hyperspace/unit_stretchedParticle.cpp
savers/hyperspace/unit_tunnel.cpp
savers/hyperspace/unit_wavyNormalCubeMaps.cpp
savers/hyperspace/hyperspace_port.cpp
savers/lattice/lattice_unit.cpp
savers/lattice/lattice_port.cpp
# Skyrocket. soundEngine.cpp is NOT built and its ~7MB of samples are not shipped: upstream
# drives OpenAL, the port runs with dSound = 0, and soundEngine.h is a stub.
savers/skyrocket/unit_flare.cpp
savers/skyrocket/unit_particle.cpp
savers/skyrocket/unit_shockwave.cpp
savers/skyrocket/unit_skyrocket.cpp
savers/skyrocket/unit_smoke.cpp
savers/skyrocket/unit_world.cpp
savers/skyrocket/skyrocket_port.cpp
savers/rslibs/rsMath/rsVec.cpp
savers/rslibs/rsMath/rsVec4.cpp
savers/rslibs/rsMath/rsMatrix.cpp
savers/rslibs/rsMath/rsQuat.cpp
savers/rslibs/Rgbhsl/Rgbhsl.cpp
# Implicit surfaces: Hyperspace's "goo" is a marching-cubes isosurface.
savers/rslibs/Implicit/impSurface.cpp
savers/rslibs/Implicit/impCubeVolume.cpp
savers/rslibs/Implicit/impCubeTables.cpp
savers/rslibs/Implicit/impShape.cpp
savers/rslibs/Implicit/impSphere.cpp
savers/rslibs/Implicit/impCapsule.cpp
savers/rslibs/Implicit/impEllipsoid.cpp
savers/rslibs/Implicit/impHexahedron.cpp
savers/rslibs/Implicit/impRoundedHexahedron.cpp
savers/rslibs/Implicit/impKnot.cpp
savers/rslibs/Implicit/impTorus.cpp
)
target_include_directories(savers PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/savers
${CMAKE_CURRENT_SOURCE_DIR}/savers/compat
${CMAKE_CURRENT_SOURCE_DIR}/savers/rslibs
${CMAKE_CURRENT_SOURCE_DIR}/savers/flux
${CMAKE_CURRENT_SOURCE_DIR}/savers/plasma
${CMAKE_CURRENT_SOURCE_DIR}/savers/solarwinds
${CMAKE_CURRENT_SOURCE_DIR}/savers/hyperspace
${CMAKE_CURRENT_SOURCE_DIR}/savers/lattice
${CMAKE_CURRENT_SOURCE_DIR}/savers/skyrocket
)
# RS_XSCREENSAVER picks the savers' platform-neutral path. Without it they compile their Win32
# shell and nothing links.
target_compile_definitions(savers PRIVATE RS_XSCREENSAVER)
set_target_properties(savers PROPERTIES C_STANDARD 99 CXX_STANDARD 17)
# -ffast-math for the same reason as Flurry: particle simulation judged entirely by eye.
target_compile_options(savers PRIVATE -O2 -ffast-math -Wall -Wno-unused-parameter)
target_link_libraries(savers log GLESv2 m)
# ---------------------------------------------------------------------------
# Discord Social SDK bridge -> libarmsx2_discord.so
#

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