26 Commits
Author SHA1 Message Date
jpolo1224 33343cd153 Release 0.8 2026-08-15 01:26:54 -04:00
jpolo1224 6cd7866553 i18n: Brazilian Portuguese corrections
From johnpetersa19. Fixes renderer.shaderChain.pass, which read "passar" -- the
verb "to pass" rather than a rendering pass -- and its plural, translates a
label left in English, and adds the packages.* strings added after the original
translation. Also drops five strings that were stored truncated mid-sentence;
English is better than half a sentence.
2026-08-15 01:26:54 -04:00
jpolo1224 6a5ad70ec7 Updater: pick the release asset that matches this build
A release now carries four APKs rather than one, so the updater has to choose
the asset built for the device it is running on instead of taking the first it
finds. Matches on the variant suffix in the asset name and falls through to the
next release rather than giving up when one has no usable asset.
2026-08-15 01:26:54 -04:00
jpolo1224 c990f34d5f UI: frame generation controls, and route the setting to the core at all
Adds the import row for Lossless.dll, the multiplier, Performance shaders and
Motion detail, plus the strings for all of it.

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

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

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

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

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

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

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

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

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

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

Notes for anyone reading this later:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Measured on device (Mirror's Edge, MT RSX on, state-verified windows):
menu instructions -55% (402.7G -> 179.5G per 30 s), played-gameplay
instructions -29% (391.6G -> 279.7G), loop iterations down ~1,400x, wait
counts/durations unchanged, 30 fps frame pacing unchanged (max frametime
34.2 ms). Note: cpu-cycles PMU counts at full clock during WFE park on
this SoC, so cycle-based profiles cannot see this change; measure with
instructions retired.
2026-08-14 03:47:48 -05:00
jpolo1224 39ca5cdab6 0.7.2: settings fixes, Oboe by default, and a working per-section Reset
Per-section Reset did nothing on most tabs. The field lists describe the tabs
as they were before the PS3 rewrite, so Reset was clearing settings the tabs no
longer show while missing most of what they do: Performance listed 22 of 47,
Graphics 45 of 57, Audio 10 of 16 -- audioRenderer, audioFormat, audioChannels
and audioCubebBackend were absent, so changing the audio backend and pressing
Reset was a no-op. Regenerated from what each tab actually writes, mapping
ps3.foo to its ps3Foo key and validating every entry against the serialiser.
Five keys also moved off Graphics because another tab owns them, which was a
cross-tab clobber waiting to happen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Module loading also no longer abandons the remaining modules after one
object fails to load.
2026-08-13 18:36:26 -04:00
91 changed files with 6302 additions and 253 deletions
+3
View File
@@ -112,3 +112,6 @@
path = 3rdparty/protobuf/protobuf
url = ../../protocolbuffers/protobuf.git
ignore = dirty
[submodule "3rdparty/oboe/oboe"]
path = 3rdparty/oboe/oboe
url = https://github.com/google/oboe.git
+15
View File
@@ -141,6 +141,12 @@ else()
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
endif()
# Oboe (Android only)
if(ANDROID)
message(STATUS "Using static oboe from 3rdparty")
add_subdirectory(oboe EXCLUDE_FROM_ALL)
endif()
# SoundTouch
add_subdirectory(SoundTouch EXCLUDE_FROM_ALL)
@@ -367,6 +373,15 @@ add_subdirectory(fusion EXCLUDE_FROM_ALL)
# FERAL INTERACTIVE
add_subdirectory(feralinteractive EXCLUDE_FROM_ALL)
# LSFG: Lossless Scaling frame generation. Android only, and deliberately NOT EXCLUDE_FROM_ALL --
# libarmsx3_lsfg.so has to be built and packaged even though nothing links it, because the core
# reaches it by dlopen rather than by linking. Marking it excluded produces a build that succeeds
# and an APK with no frame generation in it.
#
# The subdir returns immediately when the submodule is absent, so a checkout without it still
# builds; frame generation simply reports itself unavailable at runtime.
add_subdirectory(lsfg)
# add nice ALIAS targets for ease of use
if(USE_SYSTEM_LIBUSB)
add_library(3rdparty::libusb ALIAS usb-1.0-shared)
+46
View File
@@ -0,0 +1,46 @@
diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index d89a972f5d..f64e551a51 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -2504,8 +2504,40 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
RegScavenger *RS) const {
// All calls are tail calls in GHC calling conv, and functions have no
// prologue/epilogue.
- if (MF.getFunction().getCallingConv() == CallingConv::GHC)
+ if (MF.getFunction().getCallingConv() == CallingConv::GHC) {
+ // ...but they can still need an emergency spill slot.
+ //
+ // Returning here skips every path below that reserves one, so a GHC function never gets
+ // a scavenging frame index on AArch64. That is safe only while the premise holds. It
+ // stops holding as soon as the allocator spills: the function then has real stack
+ // objects, eliminateFrameIndex may need a scratch register to materialise an offset,
+ // and GHC has reserved nearly every GPR, so there is no free register to take and no
+ // slot to spill one into. The scavenger then aborts the whole module with
+ // "Cannot scavenge register without an emergency spill slot".
+ //
+ // Reproduced with RPCS3's PPU recompiler, which emits ghccc for every guest function.
+ // A single function of Saint Seiya: The Sanctuary (BLES01421) fails this way, and losing
+ // it costs the entire module, whose functions then fall back to an interpreter loop. The
+ // failure needs ghccc AND a scheduling model that pushes pressure over the line (it
+ // reproduces on cortex-x1/x2/x3 and cortex-a55, not on cortex-a76/a78/generic) AND -O2;
+ // remove any one and the same function compiles.
+ //
+ // Gated on the function actually having a frame, so a GHC function with no stack objects
+ // still gets no prologue and nothing changes for it. The cost where it does apply is one
+ // 8-byte slot.
+ MachineFrameInfo &GHCMFI = MF.getFrameInfo();
+
+ if (RS && GHCMFI.estimateStackSize(MF) > 0) {
+ const TargetRegisterInfo *TRI = MF.getSubtarget().getRegisterInfo();
+ const TargetRegisterClass &RC = AArch64::GPR64RegClass;
+ int FI = GHCMFI.CreateSpillStackObject(TRI->getSpillSize(RC), TRI->getSpillAlign(RC));
+ RS->addScavengingFrameIndex(FI);
+ LLVM_DEBUG(dbgs() << "GHC function with a frame, allocated fi#" << FI
+ << " as the emergency spill slot.\n");
+ }
+
return;
+ }
const AArch64Subtarget &Subtarget = MF.getSubtarget<AArch64Subtarget>();
+147
View File
@@ -0,0 +1,147 @@
# libarmsx3_lsfg.so -- Lossless Scaling frame generation, sealed away from the emulator core.
#
# The entire reason this is a separate shared object is symbol collision. volk defines 655 globals
# named vkCreateImage, vkQueueSubmit, ... and all 124 that our Vulkan loader declares in
# rpcs3/Emu/RSX/VK/vk_android_loader.h are among them. Linked into libarmsx3-core.so this either
# fails at link or, worse, merges -- and framegen's volkLoadDevice(itsOwnDevice) then repoints the
# whole RSX renderer at framegen's VkDevice. See armsx3_lsfg_shim.h.
#
# Android only. framegen's non-Android path shares images by FD, which Adreno and Mali refuse for
# AHB-imported memory, so there is nothing here worth building for desktop.
if (NOT ANDROID)
return()
endif()
set(LSFG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/lsfg-vk-android")
if (NOT EXISTS "${LSFG_ROOT}/framegen/CMakeLists.txt")
message(STATUS "LSFG: 3rdparty/lsfg/lsfg-vk-android is missing, frame generation will not be built")
return()
endif()
if (NOT EXISTS "${LSFG_ROOT}/thirdparty/volk/volk.c")
# Called out explicitly because the failure is otherwise mystifying: framegen links volk
# PUBLIC, so without it the error names framegen rather than the submodule that is missing.
message(STATUS "LSFG: thirdparty/volk is missing (git submodule update --init), skipping")
return()
endif()
# volk, built for Android.
#
# VK_USE_PLATFORM_ANDROID_KHR has to be set on VOLK ITSELF, not only on framegen. Without it volk
# never defines vkGetAndroidHardwareBufferPropertiesANDROID, and the resulting undefined symbol
# points at framegen -- sending you to debug the wrong target entirely.
add_library(armsx3_lsfg_volk STATIC "${LSFG_ROOT}/thirdparty/volk/volk.c")
target_include_directories(armsx3_lsfg_volk PUBLIC "${LSFG_ROOT}/thirdparty/volk")
target_compile_definitions(armsx3_lsfg_volk PUBLIC VK_USE_PLATFORM_ANDROID_KHR VK_NO_PROTOTYPES)
set_target_properties(armsx3_lsfg_volk PROPERTIES
POSITION_INDEPENDENT_CODE ON
C_VISIBILITY_PRESET hidden)
# framegen.
#
# Its own CMakeLists expects a target called `volk`, so alias ours rather than patching upstream.
if (NOT TARGET volk)
add_library(volk ALIAS armsx3_lsfg_volk)
endif()
add_subdirectory("${LSFG_ROOT}/framegen" "${CMAKE_CURRENT_BINARY_DIR}/framegen" EXCLUDE_FROM_ALL)
# Undo the project-wide -fno-exceptions for framegen and the shim.
#
# The top-level build sets it with add_compile_options, which every later add_subdirectory
# inherits. framegen has dozens of throw sites and they do not warn -- they fail to compile. The
# shim needs exceptions for the opposite reason: it exists to CATCH them so none reach the dlopen
# boundary.
foreach (tgt lsfg-vk-framegen)
if (TARGET ${tgt})
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
# PRIVATE, not PUBLIC: leaking this onto consumers collides with the valueless #define
# our own Vulkan headers use, in hundreds of RSX translation units.
target_compile_definitions(${tgt} PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
endif()
endforeach()
# Shader extraction: DXBC out of the user's own Lossless.dll, translated to SPIR-V.
#
# framegen asks for SPIR-V by name and does not read the DLL itself, so this chain is the caller's
# responsibility. Building upstream's own libraries rather than writing a DXBC translator: dxbc is
# DXVK's, and reimplementing it would be absurd.
#
# Optional. Without these the library still builds and frame generation still reports itself
# available -- it just cannot initialize until shaders exist, which is also what happens when the
# user has not supplied a DLL.
set(LSFG_HAS_EXTRACT OFF)
if (EXISTS "${LSFG_ROOT}/thirdparty/dxbc/CMakeLists.txt" AND
EXISTS "${LSFG_ROOT}/thirdparty/pe-parse/CMakeLists.txt")
# pe-parse defaults to a shared library and command-line tools, neither of which belongs in
# an APK. Forced here because its options are plain option(), so they take whatever is
# already in the cache unless overridden.
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(BUILD_COMMAND_LINE_TOOLS OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(PEPARSE_ENABLE_TESTING OFF CACHE BOOL "" FORCE)
add_subdirectory("${LSFG_ROOT}/thirdparty/dxbc" "${CMAKE_CURRENT_BINARY_DIR}/dxbc" EXCLUDE_FROM_ALL)
add_subdirectory("${LSFG_ROOT}/thirdparty/pe-parse" "${CMAKE_CURRENT_BINARY_DIR}/pe-parse" EXCLUDE_FROM_ALL)
foreach (tgt dxbc pe-parse)
if (TARGET ${tgt})
# Same -fno-exceptions problem as framegen: both throw, and inheriting the
# project-wide flag turns that into a compile error rather than a warning.
target_compile_options(${tgt} PRIVATE -fexceptions)
set_target_properties(${tgt} PROPERTIES
POSITION_INDEPENDENT_CODE ON
CXX_VISIBILITY_PRESET hidden)
set(LSFG_HAS_EXTRACT ON)
endif()
endforeach()
endif()
add_library(armsx3_lsfg SHARED armsx3_lsfg_shim.cpp)
target_include_directories(armsx3_lsfg PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${LSFG_ROOT}/framegen/public")
target_compile_options(armsx3_lsfg PRIVATE -fexceptions)
target_compile_definitions(armsx3_lsfg PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
set_target_properties(armsx3_lsfg PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
OUTPUT_NAME "armsx3_lsfg")
target_link_libraries(armsx3_lsfg PRIVATE lsfg-vk-framegen armsx3_lsfg_volk android log)
if (LSFG_HAS_EXTRACT)
target_sources(armsx3_lsfg PRIVATE
"${LSFG_ROOT}/src/extract/trans.cpp"
"${LSFG_ROOT}/src/extract/extract.cpp")
target_include_directories(armsx3_lsfg PRIVATE "${LSFG_ROOT}/include")
target_link_libraries(armsx3_lsfg PRIVATE dxbc pe-parse)
target_compile_definitions(armsx3_lsfg PRIVATE ARMSX3_LSFG_HAVE_EXTRACT=1)
message(STATUS "LSFG: shader extraction enabled (dxbc + pe-parse)")
else()
message(STATUS "LSFG: shader extraction NOT available, frame generation cannot initialize")
endif()
# Keep the exported surface to the shim alone.
#
# The version script is what makes the isolation real rather than aspirational: without it,
# framegen's and volk's symbols are still dynamic and the loader can bind our renderer's vk* to
# them. Verify with:
# llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
# Only armsx3_lsfg_* may appear. Any vk* or LSFG_3_1 symbol means this stopped working.
target_link_options(armsx3_lsfg PRIVATE
"-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/armsx3_lsfg.map"
"-Wl,--no-undefined")
+32
View File
@@ -0,0 +1,32 @@
/* Exported surface of libarmsx3_lsfg.so.
*
* This list IS the isolation. framegen and volk are statically linked into this library and
* between them define 655 globals named vkCreateImage, vkQueueSubmit, ... -- 124 of which are
* exactly the names libarmsx3-core.so's Vulkan loader declares. If any of those stay dynamic,
* the loader is free to bind the renderer's entry points to framegen's copies, and framegen's
* volkLoadDevice() has already pointed those at a different VkDevice.
*
* -fvisibility=hidden covers most of it; this covers the rest, including anything upstream marks
* __attribute__((visibility("default"))) -- which framegen's public API does.
*
* Check it, do not assume it:
* llvm-nm --defined-only --extern-only libarmsx3_lsfg.so
* Nothing but armsx3_lsfg_* should be listed.
*/
{
global:
armsx3_lsfg_abi_version;
armsx3_lsfg_initialize;
armsx3_lsfg_create_context_ahb;
armsx3_lsfg_present;
armsx3_lsfg_destroy_context;
armsx3_lsfg_wait_idle;
armsx3_lsfg_finalize;
armsx3_lsfg_last_error;
armsx3_lsfg_import_shaders;
armsx3_lsfg_shader_count;
armsx3_lsfg_get_shader;
local:
*;
};
+404
View File
@@ -0,0 +1,404 @@
// Implementation of the C ABI in armsx3_lsfg_shim.h.
//
// This translation unit is the ONLY thing in libarmsx3_lsfg.so that anyone outside it may touch.
// Everything else -- framegen, volk, and volk's 655 vk* globals -- stays hidden behind
// -fvisibility=hidden so the dynamic linker cannot bind our renderer's vkCmdDraw to framegen's
// copy. See the header for why that matters.
//
// Rules for every entry point here:
// * no C++ type crosses the boundary (separate libc++ per .so under c++_static),
// * no exception crosses the boundary (framegen throws; dlopen'd code must not),
// * a failure returns a code and leaves a message in armsx3_lsfg_last_error().
#include "armsx3_lsfg_shim.h"
#include <lsfg_3_1.hpp>
#include <lsfg_3_1p.hpp>
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
#include <extract/extract.hpp>
#include <extract/trans.hpp>
#include <config/config.hpp>
#endif
#include <exception>
#include <map>
#include <string>
#include <vector>
namespace
{
// thread_local because the renderer and whatever calls initialize() are not the same thread,
// and a shared buffer would let one overwrite the other's message mid-report.
thread_local std::string g_last_error;
bool g_initialized = false;
// Which shader family initialize() chose. Fixed until finalize(): LSFG_3_1 and LSFG_3_1P keep
// entirely separate device state and context tables, so a context created by one cannot be
// presented or destroyed through the other -- every entry point below has to dispatch on this.
bool g_performance = false;
void clear_error()
{
g_last_error.clear();
}
void set_error(const char* what)
{
g_last_error = what ? what : "unknown error";
}
void set_error(const std::string& what)
{
g_last_error = what.empty() ? "unknown error" : what;
}
}
// Wrap a call so nothing escapes.
//
// catch (...) rather than catching LSFG's types: framegen throws several, they are not part of
// its public header, and an exception reaching the dlopen boundary is undefined behaviour -- so
// the exact type matters less than the guarantee that none of them get out.
#define ARMSX3_LSFG_GUARD(expr, failure_result) \
try \
{ \
clear_error(); \
expr; \
} \
catch (const std::exception& e) \
{ \
set_error(e.what()); \
return (failure_result); \
} \
catch (...) \
{ \
set_error("unknown exception from framegen"); \
return (failure_result); \
}
extern "C" uint32_t armsx3_lsfg_abi_version(void)
{
return ARMSX3_LSFG_ABI_VERSION;
}
extern "C" const char* armsx3_lsfg_last_error(void)
{
return g_last_error.c_str();
}
extern "C" int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user)
{
if (!loader)
{
set_error("no shader loader supplied");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
// The std::function is built HERE, on framegen's side of the boundary, from a plain C
// function pointer. That is the whole point of taking a function pointer in the header: an
// std::function constructed by the core would be a different type under a different libc++.
//
// Throwing out of this lambda is how a missing shader is reported to framegen, which is what
// it expects -- and the throw stays inside this .so, caught by the guard below.
const auto bridge = [loader, user](const std::string& name) -> std::vector<uint8_t>
{
const uint8_t* data = nullptr;
uint32_t size = 0;
if (loader(name.c_str(), &data, &size, user) != ARMSX3_LSFG_OK || !data || !size)
{
throw std::runtime_error("shader not available: " + name);
}
return std::vector<uint8_t>(data, data + size);
};
// Recorded BEFORE the call so the guard's failure path cannot leave the two disagreeing.
g_performance = performance != 0;
if (g_performance)
{
ARMSX3_LSFG_GUARD(
LSFG_3_1P::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
else
{
ARMSX3_LSFG_GUARD(
LSFG_3_1::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge),
ARMSX3_LSFG_ERR_SHADERS)
}
g_initialized = true;
return ARMSX3_LSFG_OK;
}
extern "C" int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
if (!in0 || !in1 || !out_n || !out_count)
{
set_error("null image or empty output set");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
int32_t id = ARMSX3_LSFG_ERR_UNKNOWN;
// AHardwareBuffer* arrives as void* so the header stays free of android/hardware_buffer.h,
// which the core has no reason to include.
std::vector<AHardwareBuffer*> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(static_cast<AHardwareBuffer*>(out_n[i]));
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1P::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(
id = LSFG_3_1::createContextFromAHB(
static_cast<AHardwareBuffer*>(in0), static_cast<AHardwareBuffer*>(in1), outs,
VkExtent2D{width, height}, static_cast<VkFormat>(format)),
ARMSX3_LSFG_ERR_VULKAN)
}
return id;
}
extern "C" int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count)
{
if (!g_initialized)
{
set_error("not initialized");
return ARMSX3_LSFG_ERR_NOT_INITIALIZED;
}
std::vector<int> outs;
outs.reserve(out_count);
for (uint32_t i = 0; i < out_count; ++i)
{
outs.push_back(out_sems ? out_sems[i] : -1);
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::presentContext(ctx, in_sem, outs), ARMSX3_LSFG_ERR_VULKAN)
}
return ARMSX3_LSFG_OK;
}
extern "C" int armsx3_lsfg_destroy_context(int32_t ctx)
{
if (!g_initialized)
{
return ARMSX3_LSFG_OK; // nothing to release
}
if (g_performance)
{
ARMSX3_LSFG_GUARD(LSFG_3_1P::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
else
{
ARMSX3_LSFG_GUARD(LSFG_3_1::deleteContext(ctx), ARMSX3_LSFG_ERR_UNKNOWN)
}
return ARMSX3_LSFG_OK;
}
extern "C" void armsx3_lsfg_wait_idle(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::waitIdle(); else LSFG_3_1::waitIdle();
}
catch (...)
{
// Deliberately swallowed and not recorded: this is called on the present path, and a
// failure to wait is reported by whatever uses the images next. Setting the error string
// here would overwrite a more useful message from the call that actually failed.
}
}
extern "C" void armsx3_lsfg_finalize(void)
{
if (!g_initialized)
{
return;
}
try
{
if (g_performance) LSFG_3_1P::finalize(); else LSFG_3_1::finalize();
}
catch (...)
{
}
g_initialized = false;
}
#ifdef ARMSX3_LSFG_HAVE_EXTRACT
// Satisfy the one symbol upstream's extract.cpp needs from its config layer.
//
// It reads exactly one field, Config::activeConf.dll, to find the file. Defining the object here
// rather than compiling their config module avoids dragging in toml11 and a config-file format
// that has no meaning inside an APK -- the path comes from the user's file picker instead.
namespace Config { Configuration activeConf; }
namespace
{
// name -> SPIR-V, translated once at import.
std::map<std::string, std::vector<uint8_t>> g_shaders;
}
extern "C" int armsx3_lsfg_import_shaders(const char* dll_path)
{
if (!dll_path || !*dll_path)
{
set_error("no file selected");
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
clear_error();
g_shaders.clear();
// Upstream's own shader names, both families.
//
// Taken verbatim from nameIdxTable in extract.cpp rather than guessed -- a made-up name fails
// as "Shader hash not found", which reads like a corrupt DLL and is not.
//
// Two sets: the plain names are LSFG 3.1 and the p_ prefixed ones are 3.1p. Which family gets
// used depends on which framegen entry point runs, so both are extracted and whatever the DLL
// actually contains is kept. Missing names are skipped rather than fatal, because a given
// Lossless Scaling version legitimately ships only one family.
static const char* const k_names[] = {
"mipmaps", "alpha[0]", "alpha[1]", "alpha[2]", "alpha[3]",
"beta[0]", "beta[1]", "beta[2]", "beta[3]", "beta[4]",
"gamma[0]", "gamma[1]", "gamma[2]", "gamma[3]", "gamma[4]",
"delta[0]", "delta[1]", "delta[2]", "delta[3]", "delta[4]",
"delta[5]", "delta[6]", "delta[7]", "delta[8]", "delta[9]",
"generate",
"p_mipmaps", "p_alpha[0]", "p_alpha[1]", "p_alpha[2]", "p_alpha[3]",
"p_beta[0]", "p_beta[1]", "p_beta[2]", "p_beta[3]", "p_beta[4]",
"p_gamma[0]", "p_gamma[1]", "p_gamma[2]", "p_gamma[3]", "p_gamma[4]",
"p_delta[0]", "p_delta[1]", "p_delta[2]", "p_delta[3]", "p_delta[4]",
"p_delta[5]", "p_delta[6]", "p_delta[7]", "p_delta[8]", "p_delta[9]",
"p_generate",
};
try
{
Config::activeConf.dll = dll_path;
Extract::extractShaders();
for (const char* name : k_names)
{
// getShader hands back DXBC; framegen wants SPIR-V. Translating at import rather than
// on demand keeps the cost off the present path entirely.
//
// Individually guarded: a DLL that ships only one shader family throws on every name
// in the other, and that is normal rather than a failure of the import.
try
{
auto spirv = Extract::translateShader(Extract::getShader(name));
if (!spirv.empty())
{
g_shaders[name] = std::move(spirv);
}
}
catch (const std::exception&)
{
// Not in this DLL. Keep going.
}
}
if (g_shaders.empty())
{
set_error("no usable shaders in that file -- is it Lossless.dll from Lossless Scaling?");
return ARMSX3_LSFG_ERR_SHADERS;
}
}
catch (const std::exception& e)
{
g_shaders.clear();
set_error(e.what());
return ARMSX3_LSFG_ERR_SHADERS;
}
catch (...)
{
g_shaders.clear();
set_error("unknown failure reading the file");
return ARMSX3_LSFG_ERR_SHADERS;
}
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_shader_count(void)
{
return static_cast<int>(g_shaders.size());
}
extern "C" int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size)
{
if (!name || !out_data || !out_size)
{
return ARMSX3_LSFG_ERR_BAD_ARGUMENT;
}
const auto it = g_shaders.find(name);
if (it == g_shaders.end() || it->second.empty())
{
return ARMSX3_LSFG_ERR_SHADERS;
}
*out_data = it->second.data();
*out_size = static_cast<uint32_t>(it->second.size());
return ARMSX3_LSFG_OK;
}
#else
extern "C" int armsx3_lsfg_import_shaders(const char*)
{
set_error("this build has no shader extraction support");
return ARMSX3_LSFG_ERR_SHADERS;
}
extern "C" int armsx3_lsfg_shader_count(void) { return 0; }
extern "C" int armsx3_lsfg_get_shader(const char*, const uint8_t**, uint32_t*)
{
return ARMSX3_LSFG_ERR_SHADERS;
}
#endif
+142
View File
@@ -0,0 +1,142 @@
// C ABI for Lossless Scaling frame generation.
//
// framegen CANNOT be linked into libarmsx3-core.so. It links volk, which defines 655 globals
// named vkCreateImage, vkQueueSubmit, ... and 124 of those are byte-for-byte the names our own
// Vulkan loader declares in rpcs3/Emu/RSX/VK/vk_android_loader.h -- every single symbol the RSX
// renderer uses. Two ways that goes wrong, and the second is the one that costs a week:
//
// 1. duplicate symbol at link time (clang defaults to -fno-common), or
// 2. the linker merges them, and framegen's volkLoadDevice(itsOwnDevice) then repoints every
// entry point the renderer uses at framegen's VkDevice. Every later vkCmdDraw goes to the
// wrong device, and it presents as a driver crash with nothing pointing at frame generation.
//
// So framegen and volk live in their own libarmsx3_lsfg.so, reached by dlopen + dlsym through
// this header. Nothing here is C++: the CMake project builds ANDROID_STL=c++_static, so each .so
// carries its own libc++ and an std::vector or std::function crossing the boundary would be two
// unrelated types that happen to share a name. The shim builds those on its own side.
//
// framegen also throws (LSFG::vulkan_error and friends). Exceptions must not cross a dlopen
// boundary either, so every entry point here catches everything and returns a code; the message
// is retrievable with armsx3_lsfg_last_error().
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// Bump when anything below changes shape. The loader refuses a library whose version it does not
// recognise, so a stale libarmsx3_lsfg.so on a user's device fails loudly at load instead of
// quietly passing mismatched structs.
#define ARMSX3_LSFG_ABI_VERSION 2u
// Mark the exported surface explicitly.
//
// The library is built -fvisibility=hidden so framegen's and volk's symbols stay in, and a
// version script narrows the dynamic table further. Neither of those can PROMOTE a symbol: a
// function hidden at compile time is local in the object, and `global:` in the linker script
// cannot bring it back. Without this attribute the .so builds and exports nothing at all, and
// the failure only shows up as dlsym returning null at runtime.
#if defined(__GNUC__) || defined(__clang__)
#define ARMSX3_LSFG_API __attribute__((visibility("default")))
#else
#define ARMSX3_LSFG_API
#endif
enum armsx3_lsfg_result
{
ARMSX3_LSFG_OK = 0,
ARMSX3_LSFG_ERR_UNKNOWN = -1,
ARMSX3_LSFG_ERR_NOT_INITIALIZED = -2,
ARMSX3_LSFG_ERR_BAD_ARGUMENT = -3,
ARMSX3_LSFG_ERR_SHADERS = -4,
ARMSX3_LSFG_ERR_VULKAN = -5,
};
// Hand back the SPIR-V for a named shader.
//
// framegen does NOT read Lossless.dll -- it asks for shaders by name and expects SPIR-V back.
// Extracting them from the user's own copy (PE resource -> DXBC -> SPIR-V) is the caller's job,
// which is deliberate: the shaders are THS's property and nothing here ships or downloads them.
//
// Return ARMSX3_LSFG_OK and set *out_data / *out_size on success. The buffer must stay valid
// until the initialize() call that triggered this returns. Any other return means "no such
// shader" and fails initialization.
typedef int (*armsx3_lsfg_shader_loader)(const char* name, const uint8_t** out_data,
uint32_t* out_size, void* user);
// Version of the loaded library. Call first; anything else on a mismatched library is undefined.
ARMSX3_LSFG_API uint32_t armsx3_lsfg_abi_version(void);
// Bring up framegen on the adapter identified by device_uuid (VkPhysicalDeviceIDProperties
// deviceUUID, 16 bytes, passed as the first 8 -- that is what framegen matches on).
//
// framegen creates its OWN VkDevice on that adapter. It does not share ours, which is why images
// have to be handed over as AHardwareBuffer below rather than as VkImage.
// performance selects framegen's 3.1p shader family instead of 3.1: a cheaper pipeline at lower
// quality, which is the difference between usable and not on a mobile GPU. It is fixed for the
// lifetime of the library state -- every context, present and teardown after this call goes to the
// family chosen here, because the two keep separate contexts and separate device state.
//
// flow_scale is the optical-flow resolution as a fraction of full: 1.0 is upstream's default and
// lower is cheaper. Note the sense is inverted from upstream's own config file, which stores a
// divisor and passes 1.0f/value here.
ARMSX3_LSFG_API int armsx3_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint64_t generation_count, int performance, armsx3_lsfg_shader_loader loader, void* user);
// Create a context over a set of shared images.
//
// AHardwareBuffer rather than the FD path framegen also offers, because Adreno and Mali both
// refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory -- the FD path simply does not work
// on the hardware this port runs on.
//
// The caller keeps ownership of every AHardwareBuffer and must keep them alive until the context
// is destroyed. Returns a context id >= 0, or a negative armsx3_lsfg_result.
ARMSX3_LSFG_API int32_t armsx3_lsfg_create_context_ahb(void* in0, void* in1, void* const* out_n,
uint32_t out_count, uint32_t width, uint32_t height, int32_t format);
// Generate frames for one presented pair.
//
// Semaphores are sync file descriptors, not VkSemaphore: framegen is on a different device and a
// VkSemaphore handle would be meaningless to it. in_sem is waited on before generation starts;
// each out_sems[i] is signalled when output image i is ready. Pass -1 for an unused slot.
ARMSX3_LSFG_API int armsx3_lsfg_present(int32_t ctx, int in_sem, const int* out_sems, uint32_t out_count);
ARMSX3_LSFG_API int armsx3_lsfg_destroy_context(int32_t ctx);
// Read the user's own Lossless.dll and keep the shaders it contains.
//
// Nothing is bundled or downloaded: the shaders are THS's property and the user must supply a
// legitimately purchased copy. Only the extracted SPIR-V is kept -- the DLL itself is not needed
// afterwards and the caller may delete its copy.
//
// The work is PE resource walk -> DXBC -> SPIR-V, and it is slow enough to be worth doing once
// and caching rather than at every boot. Returns the number of shaders extracted, or a negative
// armsx3_lsfg_result; armsx3_lsfg_last_error() explains a failure in terms a user can act on
// ("is Lossless Scaling up to date?" rather than a resource id).
ARMSX3_LSFG_API int armsx3_lsfg_import_shaders(const char* dll_path);
// How many shaders are currently held. Zero means frame generation cannot start.
ARMSX3_LSFG_API int armsx3_lsfg_shader_count(void);
// Serve a previously imported shader by name, for initialize()'s loader.
//
// Pass a null loader to armsx3_lsfg_initialize to use these instead of supplying your own.
ARMSX3_LSFG_API int armsx3_lsfg_get_shader(const char* name, const uint8_t** out_data, uint32_t* out_size);
// Block until framegen's device is idle.
//
// Needed on Android because framegen's device reads AHBs that OUR device writes, and there is no
// semaphore shared between the two. Without this the read races the write. It is also the reason
// frame generation cannot be free here: this is a device-level stall, not a queue wait.
ARMSX3_LSFG_API void armsx3_lsfg_wait_idle(void);
ARMSX3_LSFG_API void armsx3_lsfg_finalize(void);
// Message for the last failing call on this thread, or "" if none. Never null.
ARMSX3_LSFG_API const char* armsx3_lsfg_last_error(void);
#ifdef __cplusplus
}
#endif
+8
View File
@@ -0,0 +1,8 @@
# Oboe
#
# Android-only. Oboe wraps AAudio (and OpenSL ES on older devices) and carries a
# per-device quirks database plus stream-restart handling, which is the part that
# matters on the low-end parts where plain AAudio glitches.
add_subdirectory(oboe EXCLUDE_FROM_ALL)
add_library(3rdparty::oboe ALIAS oboe)
Vendored Submodule
+1
Submodule 3rdparty/oboe/oboe added at 0da326e4ef
+34 -3
View File
@@ -1,3 +1,4 @@
#include <cerrno>
#include "File.h"
#include "mutex.h"
#include "StrFmt.h"
@@ -684,8 +685,20 @@ namespace fs
u64 result = 0;
// Loop because (huge?) read can be processed partially
while (auto r = ::read(m_fd, buffer, count))
for (;;)
{
const auto r = ::read(m_fd, buffer, count);
// EINTR is benign -- a signal landed mid-syscall -- and must be retried, not treated
// as failure. Android app storage is FUSE-backed, where this genuinely happens, and
// the ensure() below turns it into a process abort. Ported in spirit from
// ouroboros420/rpcsx (92144f094).
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break; // EOF
ensure(r > 0); // "file::read"
count -= r;
result += r;
@@ -702,8 +715,17 @@ namespace fs
u64 result = 0;
// For safety; see read()
while (auto r = ::pread(m_fd, buffer, count, offset))
for (;;)
{
const auto r = ::pread(m_fd, buffer, count, offset);
// See read(): retry EINTR rather than aborting.
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break; // EOF
ensure(r > 0); // "file::read_at"
count -= r;
offset += r;
@@ -721,8 +743,17 @@ namespace fs
u64 result = 0;
// For safety; see read()
while (auto r = ::write(m_fd, buffer, count))
for (;;)
{
const auto r = ::write(m_fd, buffer, count);
// See read(): retry EINTR rather than aborting.
if (r < 0 && errno == EINTR)
{
continue;
}
if (!r) break;
ensure(r > 0); // "file::write"
count -= r;
result += r;
+39
View File
@@ -618,6 +618,22 @@ std::string jit_compiler::cpu(std::string_view _cpu)
m_cpu = fallback_cpu_detection();
}
#ifdef ARCH_ARM64
// Detection reads the MIDR of whichever core happens to be running, so on big.LITTLE it
// can name a small in-order core. JIT'd code runs on every core, so scheduling for the
// smallest one is the wrong default -- fall back to the same wide out-of-order baseline
// used when detection fails outright. Only the schedule/cost model is affected: the
// instruction set still comes from setMAttrs (HWCAP-gated), so this can never emit an
// illegal instruction. Ported from ouroboros420/rpcsx (cc3a18e29), widened to the
// A5xx little cores that modern SoCs actually ship.
if (m_cpu == "cortex-a34" || m_cpu == "cortex-a35" || m_cpu == "cortex-a53" ||
m_cpu == "cortex-a55" || m_cpu == "cortex-a510" || m_cpu == "cortex-a520")
{
jit_log.notice("CPU detection named a little core ('%s'); using cortex-a78 as the schedule baseline.", m_cpu);
m_cpu = "cortex-a78";
}
#endif
if (m_cpu == "sandybridge" ||
m_cpu == "ivybridge" ||
m_cpu == "haswell" ||
@@ -754,6 +770,29 @@ jit_compiler::jit_compiler(const std::unordered_map<std::string, u64>& _link, st
fmt::throw_exception("LLVM Emergency Exit Invoked: '%s'", out);
}, nullptr);
// A separate handler from the fatal one -- LLVM installs and dispatches the two
// independently. Without this, an allocation failure inside LLVM (SmallVector growth
// while codegenning the enormous PPU symbol-resolver module, say) writes "LLVM ERROR:
// out of memory" to fd 2 -- which goes nowhere in an Android app -- and calls abort():
// a signal-6 death with nothing whatsoever in the log. Route it through the same
// recoverable path as the fatal handler, so a guarded compile survives and anything
// else at least says why it died. Allocating inside a bad-alloc handler is
// best-effort, but the failures here are huge single allocations, so a short log
// string still succeeds. Ported from ouroboros420/rpcsx (39a6a4c36).
llvm::remove_bad_alloc_error_handler();
llvm::install_bad_alloc_error_handler([](void*, const char* msg, bool)
{
const std::string_view out = msg ? msg : "";
if (g_llvm_fatal_message)
{
*g_llvm_fatal_message = out;
thread_ctrl::silent_exit();
}
fmt::throw_exception("LLVM Out Of Memory: '%s'", out);
}, nullptr);
return true;
}();
+66 -2
View File
@@ -2191,7 +2191,32 @@ bool handle_access_violation(u32 addr, bool is_writing, bool is_exec, ucontext_t
if (g_tls_access_violation_recovered != addr)
{
vm_log.notice("\n%s", dump_useful_thread_info());
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");
// Name a guest halt for what it is.
//
// The SPU recompilers implement the HALT family (HGT/HEQ/HLGT and friends) by
// storing to 0xffdead00 on purpose, so the fault handler catches it -- see
// make_halt in SPULLVMRecompiler.cpp and its ASMJIT counterpart. Reported as a
// bare access violation it reads like an emulator crash at a nonsense address,
// and it is neither: those instructions are assertions the GAME compiled into
// its own SPU code, so reaching one means the program checked its state, found
// it wrong, and stopped itself. The interesting question is what fed it bad
// data, which is a completely different investigation from a stray pointer.
//
// The interpreter already says "Halt" here; only the recompiled path was
// silent about it. Hit on Eternal Sonata (BLJS10017), whose TCX_CellSpursKernel0
// halts and takes the game's forward progress with it.
if (addr >= 0xffdead00 && addr < 0xffdeae00)
{
vm_log.always()("[%s] SPU halted itself: the guest executed a HALT instruction"
" (trap store to 0x%x). This is the game's own assertion firing, not a bad"
" pointer -- something upstream handed it state it rejected.",
cpu->get_name(), addr);
}
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");
}
}
// TODO:
@@ -2544,13 +2569,38 @@ static void signal_handler(int /*sig*/, siginfo_t* info, void* uct) noexcept
const bool is_executing = err & 0x10;
const bool is_writing = err & 0x2;
#elif defined(ARCH_ARM64)
const bool is_executing = uptr(info->si_addr) == uptr(RIP(context));
// Guess, replaced below by the hardware's own answer wherever that is available.
//
// This comparison is a heuristic and it decides something load-bearing: is_executing gates
// EVERY recovery path in this handler, so getting it wrong does not merely mislabel a log
// line, it skips handle_access_violation entirely and kills the thread. A data access whose
// faulting address happens to coincide with the PC is classified as an instruction fetch and
// takes that path, and the guest addresses most likely to collide are exactly the ones our
// own mappings sit at.
bool is_executing = uptr(info->si_addr) == uptr(RIP(context));
#if defined(__linux__) || defined(__APPLE__)
// Current CPU state decoder is reverse-engineered from the linux kernel and may not work on other platforms.
const auto decoded_reason = aarch64::decode_fault_reason(context);
const bool is_writing = (decoded_reason == aarch64::fault_reason::data_write);
// ESR_EL1 says what the fault actually was, so prefer it over the address comparison.
//
// Only when the decode produced something meaningful: it returns 'undefined' when the signal
// frame carries no ESR record, and on that path the guess is still the best available answer.
// data_read/data_write are positive evidence that this is NOT an instruction fetch, which is
// the direction that matters -- it is what lets a genuine access violation reach the recovery
// path instead of terminating the thread.
if (decoded_reason == aarch64::fault_reason::data_read ||
decoded_reason == aarch64::fault_reason::data_write)
{
is_executing = false;
}
else if (decoded_reason == aarch64::fault_reason::instruction_execute)
{
is_executing = true;
}
if (decoded_reason != aarch64::fault_reason::data_write &&
decoded_reason != aarch64::fault_reason::data_read)
{
@@ -2746,6 +2796,20 @@ const bool s_terminate_handler_set = []() -> bool
{
std::set_terminate([]()
{
// Re-entrancy guard. Under memory exhaustion the terminate path itself allocates
// (report_fatal_error formats a message -> operator new; with -fno-exceptions a failed
// allocation calls std::terminate again), which recurses forever and buries the real
// crash under a stack of aborts. If terminate re-enters, hard-stop without allocating
// so there is exactly one clean tombstone.
// Ported from ouroboros420/rpcsx (281654906).
static atomic_t<int> s_terminating{0};
if (s_terminating.exchange(1) != 0)
{
::signal(SIGABRT, SIG_DFL);
std::abort();
}
if (IsDebuggerPresent())
{
logs::listener::sync_all();
+6 -3
View File
@@ -27,10 +27,13 @@ android {
defaultConfig {
applicationId = "com.armsx3"
minSdk = 26
// Set per variant by android/build-variants.sh: 33 for the A13 build (NDK 28), 35 for
// the A15 build (NDK 29). The core is compiled against the matching API, so these must
// agree -- an APK that installs below its core's target is a dlopen failure at boot.
minSdk = (project.findProperty("armsx3.minSdk") as String?)?.toInt() ?: 33
targetSdk = 37
versionCode = 10
versionName = "0.6"
versionCode = 14
versionName = "0.8"
// 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.
@@ -98,7 +98,6 @@
"app.bgColor.rgb": "Ciclo RGB",
"app.bgColor.rgb.desc": "Desvie continuamente o fundo através do espectro de cores, como periféricos RGB. Substitui a cor fixa abaixo. Mesma limitação acima: nenhum efeito onde o plano de fundo substituto está em uso.",
"app.blockHome": "Bloquear botĂŁo Home durante o jogo",
"app.blockHome.desc": "Fixa a tela enquanto o jogo Ă© executado, para que o botĂŁo Home ou Guide do controle nĂŁo possa ser minimizado ",
"app.bootLogo": "Animação de inicialização",
"app.bootLogo.desc": "Reproduza o vídeo de introdução do ARMSX3 quando o aplicativo for iniciado.",
"app.clearCache": "Limpar dados em cache",
@@ -414,13 +413,19 @@
"overlay.uiSize.description": "Dimensiona o preenchimento do menu/biblioteca e os tamanhos de controle. 100% = padrĂŁo.",
"overlay.uiSize.label": "Tamanho da IU (bordas)",
"packages.description": "Instale um jogo, atualização ou DLC .pkg, ou um arquivo de licença .rap. Alguns jogos precisam de ambos: o .pkg contém o conteúdo e o .rap o desbloqueia. Os títulos instalados são adicionados à sua biblioteca automaticamente, e as atualizações e DLC precisam do jogo base instalado primeiro.",
"packages.install.copyFailed": "Não foi possível copiar %s desse armazenamento. Se a unidade foi desconectada ou não havia espaço suficiente para a cópia, tente novamente com o arquivo no armazenamento interno.",
"packages.install.done": "Instalado. Ele aparecerá na sua biblioteca na próxima digitalização.",
"packages.install.failed": "Falha na instalação. O arquivo pode estar criptografado, incompleto ou não ser um pacote PS3.",
"packages.install.noRoom": "Não há espaço livre suficiente para instalar %s. Esse armazenamento não pode ser lido diretamente, então o arquivo precisa ser copiado primeiro — libere espaço ou mova o arquivo para o armazenamento interno ou para um cartão SD.",
"packages.install.unreadable": "Não foi possível abrir %s. O aplicativo que fornece esse armazenamento pode ter perdido o acesso a ele — reabra a unidade e selecione o arquivo novamente.",
"packages.installed.header": "TĂ­tulos instalados",
"packages.installing": "Instalando. Pacotes grandes podem demorar alguns minutos.",
"packages.installingFile": "Instalando %s",
"packages.licences.header": "Licenças instaladas",
"packages.multiHint": "Toque em vários arquivos para selecionar todos e confirme: as partes de um jogo dividido ou um jogo junto com sua licença .rap.",
"packages.reading": "Lendo %s",
"packages.select.action": "Escolha o arquivo",
"packages.select.external": "Escolher no USB ou cartĂŁo SD",
"packages.select.title": "Selecione um arquivo .pkg ou .rap",
"packages.title": "Instalar pacote",
"packages.uninstall": "Desinstalar",
@@ -511,7 +516,6 @@
"pad.players.help": "O PS3 possui sete portas de controle e nenhum multitap, portanto, até sete pads funcionam sem configuração. Conecte-os antes do lançamento – a ordem em que eles pressionam um botão pela primeira vez é a ordem em que são atribuídos.",
"pad.pressButton": "Aperte um botĂŁo...",
"pad.pressControllerButton": "Pressione um botão do controlador…",
"pad.pressureAmount.description": "QuĂŁo forte o modificador de pressĂŁo pressiona, para jogos sensĂ­veis Ă  pressĂŁo DualShock 2 ",
"pad.pressureAmount.label": "Quantidade do modificador de pressĂŁo",
"pad.rightStick.description": "O que o botão analógico direito envia: Analógico (padrão), Face ou Personalizado (vincule cada direção abaixo).",
"pad.rightStick.invertX.description": "Espelhe o controle direito horizontalmente - corrige \"esquerda Ă© direita\".",
@@ -617,7 +621,7 @@
"perf.llvmThreads.description": "Quantos módulos PS3 são compilados ao mesmo tempo quando um jogo é inicializado pela primeira vez. Auto usa todos os núcleos da CPU, que é mais rápido, mas precisa de muita memória – o suficiente para que grandes jogos possam executar o dispositivo e fechá-lo. Reduza este valor se um jogo fechar no meio de \"Compilando Módulos PPU\".",
"perf.llvmThreads.label": "Máximo de threads de compilação LLVM",
"perf.maxSpursThreads.description": "Limita quantos encadeamentos SPURS sĂŁo executados por grupo de encadeamentos. 6 Ă© preciso em termos de hardware. Reduzi-lo Ă© um hack que pode ajudar jogos mal encadeados em dispositivos com poucos nĂşcleos, correndo o risco de quebrar outros.",
"perf.maxSpursThreads.label": "Max SPURS Threads",
"perf.maxSpursThreads.label": "Máximo de threads SPURS",
"perf.ppuDecoder.description": "Como é executada a CPU principal (PPU) do PS3. O LLVM recompila o PowerPC para o ARM64 nativo e é enormemente mais rápido - mantenha-o, a menos que você esteja depurando. O intérprete serve apenas para diagnosticar um jogo que o LLVM está errado.",
"perf.ppuDecoder.label": "Decodificador PPU",
"perf.preferredSpuThreads.description": "Quantos threads de CPU estão reservados para trabalho pesado simultâneo de SPU. Auto permite que o RPCS3 decida a partir de sua contagem de núcleos, o que geralmente ocorre em um dispositivo portátil. Definir um valor muito alto deixa o PPU sem energia.",
@@ -723,7 +727,6 @@
"renderer.clearShaderCache.alreadyEmpty": "O cache do shader já está vazio.",
"renderer.clearShaderCache.description": "Limpa os caches de shader/pipeline Vulkan + GL compilados. Use se um jogo for corrompido após uma troca ou atualização de driver – a próxima inicialização os reconstruirá de forma limpa.",
"renderer.clearShaderCache.label": "Limpar cache do sombreador",
"renderer.coalesceRenderPasses.description": "Agrupa empates consecutivos para o mesmo alvo em uma passagem de renderização. Ajuda no agrupamento de GPUs ",
"renderer.consoleAspect.description": "O aspecto que o PS3 emulado reporta ao jogo. O console sempre sinalizou 4:3 ou 16:9, então essas são as únicas opções reais - Auto deixa isso para o jogo. É para isso que o jogo serve; como ele é ajustado à SUA tela é a configuração abaixo.",
"renderer.consoleAspect.label": "Proporção do console",
"renderer.disableZcull.description": "Ignora totalmente as consultas de oclusão. Mais rápido, mas objetos que deveriam estar ocultos podem aparecer e sair - um hack de velocidade, não uma solução.",
@@ -810,8 +813,8 @@
"renderer.shaderChain.params.resetAll.confirmBody": "Cada parâmetro retorna aos padrões do próprio preset. Suas alterações aqui não podem ser desfeitas – se você quiser mantê-las, cancele e use “Salvar como nova predefinição” primeiro.",
"renderer.shaderChain.params.resetAll.confirmTitle": "Redefinir todos os parâmetros?",
"renderer.shaderChain.params.saveAs": "Salvar como nova predefinição…",
"renderer.shaderChain.pass": "passar",
"renderer.shaderChain.passes": "passes",
"renderer.shaderChain.pass": "etapa",
"renderer.shaderChain.passes": "etapas",
"renderer.shaderChain.passesUnknown": "custo desconhecido",
"renderer.shaderChain.preset.label": "Predefinição de sombreador",
"renderer.shaderChain.preset.none": "Nenhum",
@@ -877,9 +880,7 @@
"savestate.delete.title": "Excluir estado salvo",
"savestate.empty.description": "Crie um estado de salvamento enquanto o jogo está em execução e gerencie-o ou faça backup aqui.",
"savestate.empty.title": "Ainda não há estados salvos",
"savestate.error.hardcore": "Os estados de salvamento são desativados enquanto o modo Hardcore RetroAchievements está ativado. Desligue o Hardcore ",
"savestate.error.load": "NĂŁo foi possĂ­vel carregar esse slot.",
"savestate.error.memcardBusy": "O jogo ainda está gravando dados salvos, então o estado não foi salvo.",
"savestate.error.save": "NĂŁo foi possĂ­vel salvar nesse slot. Verifique o log de @@ANDROID_SAVESTATE@@.",
"savestate.hint": "Escolha um slot. Segure um slot ou use o botĂŁo de lixeira para excluĂ­-lo.",
"savestate.import": "Importar",
@@ -38,6 +38,9 @@ struct RPCSXApi {
void (*pause)();
void (*openHomeMenu)();
std::string (*getTitleId)();
unsigned long long (*getFramePeriodNs)();
unsigned long long (*getFrameWorkNs)();
int (*getRsxThreadTid)();
std::string (*getCurrentTrophyName)();
bool (*surfaceEvent)(JNIEnv *env, jobject surface, jint event);
void (*surfaceSizeChanged)(int width, int height);
@@ -53,6 +56,9 @@ struct RPCSXApi {
std::string (*getUser)();
std::string (*settingsGet)(std::string_view path);
bool (*settingsSet)(std::string_view path, std::string_view valueString);
int (*frameGenImportShaders)(std::string_view path);
int (*frameGenShaderCount)();
const char *(*frameGenShaderError)();
void (*settingsBeginBatch)();
void (*settingsEndBatch)();
bool (*installSplitPkg)(JNIEnv *env, const int *fds, int count, long progressId);
@@ -127,6 +133,9 @@ struct RPCSXLibrary : RPCSXApi {
result.pause = reinterpret_cast<decltype(pause)>(dlsym(handle, "_rpcsx_pause"));
result.openHomeMenu = reinterpret_cast<decltype(openHomeMenu)>(dlsym(handle, "_rpcsx_openHomeMenu"));
result.getTitleId = reinterpret_cast<decltype(getTitleId)>(dlsym(handle, "_rpcsx_getTitleId"));
result.getFramePeriodNs = reinterpret_cast<decltype(getFramePeriodNs)>(dlsym(handle, "_rpcsx_getFramePeriodNs"));
result.getFrameWorkNs = reinterpret_cast<decltype(getFrameWorkNs)>(dlsym(handle, "_rpcsx_getFrameWorkNs"));
result.getRsxThreadTid = reinterpret_cast<decltype(getRsxThreadTid)>(dlsym(handle, "_rpcsx_getRsxThreadTid"));
result.getCurrentTrophyName = reinterpret_cast<decltype(getCurrentTrophyName)>(dlsym(handle, "_rpcsx_getCurrentTrophyName"));
result.surfaceEvent = reinterpret_cast<decltype(surfaceEvent)>(dlsym(handle, "_rpcsx_surfaceEvent"));
result.surfaceSizeChanged = reinterpret_cast<decltype(surfaceSizeChanged)>(dlsym(handle, "_rpcsx_surfaceSizeChanged"));
@@ -141,6 +150,12 @@ struct RPCSXLibrary : RPCSXApi {
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
result.settingsSet = reinterpret_cast<decltype(settingsSet)>(dlsym(handle, "_rpcsx_settingsSet"));
// Resolved without ensure(): a core built before frame generation existed simply has no such
// symbol, and refusing to load it over a missing optional feature would be worse than the
// feature being absent. The Kotlin side treats a null here as "unsupported".
result.frameGenImportShaders = reinterpret_cast<decltype(frameGenImportShaders)>(dlsym(handle, "_rpcsx_frameGenImportShaders"));
result.frameGenShaderCount = reinterpret_cast<decltype(frameGenShaderCount)>(dlsym(handle, "_rpcsx_frameGenShaderCount"));
result.frameGenShaderError = reinterpret_cast<decltype(frameGenShaderError)>(dlsym(handle, "_rpcsx_frameGenShaderError"));
result.settingsBeginBatch = reinterpret_cast<decltype(settingsBeginBatch)>(dlsym(handle, "_rpcsx_settingsBeginBatch"));
result.settingsEndBatch = reinterpret_cast<decltype(settingsEndBatch)>(dlsym(handle, "_rpcsx_settingsEndBatch"));
result.installSplitPkg = reinterpret_cast<decltype(installSplitPkg)>(dlsym(handle, "_rpcsx_installSplitPkg"));
@@ -614,6 +629,26 @@ Java_net_rpcsx_RPCSX_supportsCustomDriverLoading(JNIEnv *env,
return access("/dev/kgsl-3d0", F_OK) == 0;
}
// Force the Adreno GPU to its maximum clocks, or release it back to normal scaling.
//
// Adreno's DVFS ramps clocks up only after it has already seen load, so a scene that suddenly
// becomes GPU-bound stutters through the ramp every time it happens. Pinning the clocks removes
// that at the cost of heat and battery, which is why it is opt-in rather than a default.
//
// Nothing here talks to the emulator core: adrenotools is linked into this JNI library, and
// adrenotools_set_turbo opens /dev/kgsl-3d0 itself and silently does nothing on non-Adreno
// hardware or if the open fails. So it is safe to call unconditionally, including before the core
// is loaded. Ported from the RPCSX Android fork, which ships it default-off; kept default-off here
// for the same reason.
extern "C" JNIEXPORT void JNICALL
Java_net_rpcsx_RPCSX_setGpuTurbo(JNIEnv *, jobject, jboolean on) {
#if defined(__aarch64__)
adrenotools_set_turbo(on == JNI_TRUE);
#else
(void) on;
#endif
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_getVersion(JNIEnv *env, jobject) {
// The core is dlopen()ed separately and may not be up yet -- during
@@ -959,3 +994,49 @@ Java_net_rpcsx_RPCSX_patchSetEnabled(JNIEnv *env, jobject, jstring jhash,
unwrap(env, jserial),
unwrap(env, jappVersion), enabled);
}
// ADPF telemetry. All three return 0 when unmeasured or on a core too old to export them,
// and the Kotlin side treats 0 as "skip this update" rather than feeding the OS a bogus hint.
extern "C" JNIEXPORT jlong JNICALL
Java_net_rpcsx_RPCSX_getFramePeriodNs(JNIEnv *, jobject) {
if (rpcsxLib.getFramePeriodNs == nullptr) {
return 0;
}
return static_cast<jlong>(rpcsxLib.getFramePeriodNs());
}
extern "C" JNIEXPORT jlong JNICALL
Java_net_rpcsx_RPCSX_getFrameWorkNs(JNIEnv *, jobject) {
if (rpcsxLib.getFrameWorkNs == nullptr) {
return 0;
}
return static_cast<jlong>(rpcsxLib.getFrameWorkNs());
}
extern "C" JNIEXPORT jint JNICALL
Java_net_rpcsx_RPCSX_getRsxThreadTid(JNIEnv *, jobject) {
if (rpcsxLib.getRsxThreadTid == nullptr) {
return 0;
}
return static_cast<jint>(rpcsxLib.getRsxThreadTid());
}
extern "C" JNIEXPORT jint JNICALL
Java_net_rpcsx_RPCSX_frameGenImportShaders(JNIEnv *env, jobject, jstring path) {
if (!rpcsxLib.frameGenImportShaders) {
return -1;
}
return rpcsxLib.frameGenImportShaders(unwrap(env, path));
}
extern "C" JNIEXPORT jint JNICALL
Java_net_rpcsx_RPCSX_frameGenShaderCount(JNIEnv *, jobject) {
return rpcsxLib.frameGenShaderCount ? rpcsxLib.frameGenShaderCount() : 0;
}
extern "C" JNIEXPORT jstring JNICALL
Java_net_rpcsx_RPCSX_frameGenShaderError(JNIEnv *env, jobject) {
const char *msg = rpcsxLib.frameGenShaderError ? rpcsxLib.frameGenShaderError() : "";
return env->NewStringUTF(msg ? msg : "");
}
@@ -100,6 +100,28 @@ object Ps3PatchRepo {
return if (n >= 0) Result.Ok(n) else Result.Parse
}
/**
* Import a patch.yml the user picked themselves.
*
* No checksum here, unlike [download]: there is no publisher digest to compare a
* local file against, and the user choosing the file IS the trust decision. The
* core still parses it, so a malformed file is rejected rather than half-applied.
*
* Merges into patches/patch.yml like every other import, so a hand-added patch
* sits alongside the downloaded database instead of replacing it.
*/
fun importLocal(context: Context, uri: android.net.Uri): Result {
val yaml = runCatching {
context.contentResolver.openInputStream(uri)?.use { stream ->
stream.bufferedReader().readText()
}
}.getOrNull()
if (yaml.isNullOrBlank()) return Result.Network
val n = runCatching { RPCSX.instance.patchesImport(yaml) }.getOrDefault(-1)
return if (n >= 0) Result.Ok(n) else Result.Parse
}
/** Lowercase hex SHA-256, the form rpcs3.net sends and desktop compares against. */
private fun sha256(text: String): String =
java.security.MessageDigest.getInstance("SHA-256")
@@ -63,6 +63,8 @@ object ConfigStore {
private const val KEY_SPU_DECODER_RESTORE = "config.migrated.spuDecoderRestoreLlvm"
private const val KEY_XFLOAT_BACK_TO_APPROX = "config.migrated.xfloatBackToApprox"
private const val KEY_PRECISE_SPU_OFF = "config.migrated.preciseSpuVerifyOff"
// Oboe became the Android default in 0.7.2; move anyone still on the old Cubeb default.
private const val KEY_AUDIO_OBOE = "config.migrated.audioOboeDefault"
private const val KEY_ATOMIC_DMA_OFF = "config.migrated.atomicDmaStoresOff"
// Bumped: the first pass recorded only Vblank Rate, which did not hold on its own.
private const val KEY_VBLANK_60 = "config.migrated.frameCap60"
@@ -316,6 +318,16 @@ object ConfigStore {
}
// Oboe is the Android default now. Only move people sitting on the previous default
// (Cubeb, index 2) -- anyone who deliberately picked Null or another backend keeps it.
if (!MainActivityRuntime.prefs.getBoolean(KEY_AUDIO_OBOE, false)) {
if (raw != null && parsed.ps3.audioRenderer == 2) {
parsed = parsed.copy(ps3 = parsed.ps3.copy(audioRenderer = 4))
dirty = true
}
MainActivityRuntime.prefs.edit { putBoolean(KEY_AUDIO_OBOE, true) }
}
// The ARM64 block checksum is fixed, so the full-compare workaround can go.
if (!MainActivityRuntime.prefs.getBoolean(KEY_PRECISE_SPU_OFF, false)) {
if (raw != null && parsed.ps3.preciseSpuVerification) {
@@ -126,6 +126,14 @@ data class Ps3Settings(
* wait for their real shader instead of running through the interpreter.
*/
val shaderMode: Int = 1,
/** Lossless Scaling frame generation: 0 Off, 1 x2, 2 x3, 3 x4. Off unless the user has
* supplied shaders from their own copy -- nothing is bundled. */
val frameGeneration: Int = 0,
// Default ON: 3.1p is the cheaper of the two shader families framegen ships, and on a mobile
// GPU the full-quality path costs more than the frames it buys.
val frameGenPerformance: Boolean = true,
// Optical-flow resolution as a percentage of full; lower is cheaper and blurrier in motion.
val frameGenFlowScale: Int = 100,
val writeColorBuffers: Boolean = false,
val writeDepthBuffer: Boolean = false,
val readColorBuffers: Boolean = false,
@@ -176,6 +184,16 @@ data class Ps3Settings(
val audioTimeStretch: Boolean = false,
val audioBuffering: Boolean = true,
val audioBufferMs: Int = 34,
/** cubeb backend: 0 auto, 1 aaudio, 2 opensl, 3 audiotrack. Auto reaches AAudio on
* anything modern; opensl trades latency for far fewer underruns on weak devices. */
val audioCubebBackend: Int = 0,
/** Pin Adreno to max clocks: removes DVFS ramp-up stutter in GPU-bound scenes, costs heat
* and battery. Off by default, matching the RPCSX fork this came from. */
val gpuTurbo: Boolean = false,
/** Stops the core writing any log after startup. A real performance lever on games that
* log heavily, but it also destroys the only artifact a bug report can carry, so it is
* off by default and the UI says so plainly. */
val silenceAllLogs: Boolean = false,
val netEnabled: Boolean = false,
val psnStatus: Boolean = false,
val upnpEnabled: Boolean = false,
@@ -188,6 +206,12 @@ data class Ps3Settings(
* preciseSpuVerification). Accurate is a rarely-exercised path and costs
* speed, so there is no reason to sit on it.
*/
/**
* Which face button confirms in PS3 system dialogs. 0 = circle, 1 = cross, matching
* enter_button_assign. Japanese-region games and hardware confirm with circle; the rest of
* the world uses cross, which is why RPCS3 exposes it rather than deriving it from region.
*/
val enterButtonAssign: Int = 1,
val spuXFloat: Int = 1,
val accurateSpuRsv: Boolean = true,
/**
@@ -239,7 +263,8 @@ data class Ps3Settings(
val debugConsoleMode: Boolean = false,
val resolution: Int = 2,
val anisoFilter: Int = 0,
val audioRenderer: Int = 2,
/** Index into Rpcs3Settings.AUDIO_RENDERERS. 4 = Oboe, the Android default (see node_audio). */
val audioRenderer: Int = 4,
/**
* Output aspect override in permille (1778 = 16:9, 1333 = 4:3), 0 = follow the game.
*
@@ -261,10 +286,17 @@ data class Ps3Settings(
val overlayPosition: Int = 0,
// RPCS3 stores these as "#RRGGBBAA" strings. Kept as packed ARGB ints here so
// the existing colour picker can drive them, and converted on the way out.
val overlayBodyColor: Int = 0xFFE138FF.toInt(),
val overlayBodyBg: Int = 0x002339FF,
val overlayTitleColor: Int = 0xF26C24FF.toInt(),
val overlayTitleBg: Int = 0x00000000,
// ARGB, because that is what Android colour ints are and what argbToRgba() converts FROM.
//
// These used to hold RPCS3's RGBA hex values verbatim (0xFFE138FF and friends), which are the
// right colours in the wrong order: argbToRgba then read the leading FF as alpha and rotated
// every channel one byte left, turning the default orange #FFE138FF into #E138FFFF. That is
// the pink the overlay has always drawn in, and it made the colour pickers look broken --
// every value the user chose was rotated the same way, so nothing ever matched.
val overlayBodyColor: Int = 0xFFFFE138.toInt(), // core #FFE138FF
val overlayBodyBg: Int = 0xFF002339.toInt(), // core #002339FF
val overlayTitleColor: Int = 0xFFF26C24.toInt(), // core #F26C24FF
val overlayTitleBg: Int = 0x00000000, // core #00000000, fully transparent
)
data class Settings(
@@ -1017,6 +1049,7 @@ data class Settings(
// surface layout. Keeping them in sync stops "Stretch" looking inert.
put("PS3/Video", "Stretch To Display Area", "bool", (displayFitMode == 1).toString())
put("PS3/Video", "Display Aspect Override", "int", ps3.displayAspect.coerceIn(0, 4000).toString())
put("PS3/Misc", "Silence All Logs", "bool", ps3.silenceAllLogs.toString())
put("PS3/Overlay", "Enabled", "bool", ps3.overlayEnabled.toString())
put("PS3/Overlay", "Detail level", "enum", ps3.overlayDetail.toString())
put("PS3/Overlay", "Enable Framerate Graph", "bool", ps3.overlayFramerateGraph.toString())
@@ -1030,6 +1063,9 @@ data class Settings(
put("PS3/Overlay", "Title Background (hex)", "string", argbToRgba(ps3.overlayTitleBg))
put("PS3/Video", "MSAA", "enum", ps3.msaaMode.toString())
put("PS3/Video", "Shader Mode", "enum", ps3.shaderMode.toString())
put("PS3/Video", "Frame Generation", "enum", ps3.frameGeneration.toString())
put("PS3/Video", "Frame Generation Performance Mode", "bool", ps3.frameGenPerformance.toString())
put("PS3/Video", "Frame Generation Flow Scale", "int", ps3.frameGenFlowScale.toString())
put("PS3/Video", "Write Color Buffers", "bool", ps3.writeColorBuffers.toString())
put("PS3/Video", "Write Depth Buffer", "bool", ps3.writeDepthBuffer.toString())
put("PS3/Video", "Read Color Buffers", "bool", ps3.readColorBuffers.toString())
@@ -1049,9 +1085,11 @@ data class Settings(
put("PS3/Audio", "Enable Time Stretching", "bool", ps3.audioTimeStretch.toString())
put("PS3/Audio", "Enable Buffering", "bool", ps3.audioBuffering.toString())
put("PS3/Audio", "Desired Audio Buffer Duration", "int", ps3.audioBufferMs.toString())
put("PS3/Audio", "Cubeb Backend", "int", ps3.audioCubebBackend.toString())
put("PS3/Net", "Internet enabled", "enum", ps3.netEnabled.toString())
put("PS3/Net", "PSN status", "enum", ps3.psnStatus.toString())
put("PS3/Net", "UPNP Enabled", "bool", ps3.upnpEnabled.toString())
put("PS3/System", "Enter button assignment", "enum", ps3.enterButtonAssign.toString())
put("PS3/Core", "SPU XFloat Accuracy", "enum", ps3.spuXFloat.toString())
put("PS3/Core", "Accurate SPU Reservations", "bool", ps3.accurateSpuRsv.toString())
put("PS3/Core", "Accurate Cache Line Stores", "bool", ps3.accurateCacheLine.toString())
@@ -1994,8 +2032,14 @@ data class Settings(
put("ps3ClocksScale", ps3.clocksScale)
put("ps3ResolutionScale", ps3.resolutionScale)
put("ps3MsaaMode", ps3.msaaMode)
put("ps3AudioCubebBackend", ps3.audioCubebBackend)
put("ps3ShaderMode", ps3.shaderMode)
put("ps3FrameGeneration", ps3.frameGeneration)
put("ps3FrameGenPerformance", ps3.frameGenPerformance)
put("ps3FrameGenFlowScale", ps3.frameGenFlowScale)
put("ps3WriteColorBuffers", ps3.writeColorBuffers)
put("ps3GpuTurbo", ps3.gpuTurbo)
put("ps3SilenceAllLogs", ps3.silenceAllLogs)
put("ps3WriteDepthBuffer", ps3.writeDepthBuffer)
put("ps3ReadColorBuffers", ps3.readColorBuffers)
put("ps3ReadDepthBuffer", ps3.readDepthBuffer)
@@ -2017,6 +2061,7 @@ data class Settings(
put("ps3NetEnabled", ps3.netEnabled)
put("ps3PsnStatus", ps3.psnStatus)
put("ps3UpnpEnabled", ps3.upnpEnabled)
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
put("ps3SpuXFloat", ps3.spuXFloat)
put("ps3AccurateSpuRsv", ps3.accurateSpuRsv)
put("ps3AccurateCacheLine", ps3.accurateCacheLine)
@@ -2329,8 +2374,14 @@ data class Settings(
clocksScale = json.optInt("ps3ClocksScale", def.ps3.clocksScale),
resolutionScale = json.optInt("ps3ResolutionScale", def.ps3.resolutionScale),
msaaMode = json.optInt("ps3MsaaMode", def.ps3.msaaMode),
audioCubebBackend = json.optInt("ps3AudioCubebBackend", def.ps3.audioCubebBackend),
shaderMode = json.optInt("ps3ShaderMode", def.ps3.shaderMode),
frameGeneration = json.optInt("ps3FrameGeneration", def.ps3.frameGeneration),
frameGenPerformance = json.optBoolean("ps3FrameGenPerformance", def.ps3.frameGenPerformance),
frameGenFlowScale = json.optInt("ps3FrameGenFlowScale", def.ps3.frameGenFlowScale),
writeColorBuffers = json.optBoolean("ps3WriteColorBuffers", def.ps3.writeColorBuffers),
gpuTurbo = json.optBoolean("ps3GpuTurbo", def.ps3.gpuTurbo),
silenceAllLogs = json.optBoolean("ps3SilenceAllLogs", def.ps3.silenceAllLogs),
writeDepthBuffer = json.optBoolean("ps3WriteDepthBuffer", def.ps3.writeDepthBuffer),
readColorBuffers = json.optBoolean("ps3ReadColorBuffers", def.ps3.readColorBuffers),
readDepthBuffer = json.optBoolean("ps3ReadDepthBuffer", def.ps3.readDepthBuffer),
@@ -2352,6 +2403,7 @@ data class Settings(
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
spuXFloat = json.optInt("ps3SpuXFloat", def.ps3.spuXFloat),
accurateSpuRsv = json.optBoolean("ps3AccurateSpuRsv", def.ps3.accurateSpuRsv),
accurateCacheLine = json.optBoolean("ps3AccurateCacheLine", def.ps3.accurateCacheLine),
@@ -2644,8 +2696,14 @@ data class Settings(
if (current.ps3.clocksScale != base.ps3.clocksScale) j.put("ps3ClocksScale", current.ps3.clocksScale)
if (current.ps3.resolutionScale != base.ps3.resolutionScale) j.put("ps3ResolutionScale", current.ps3.resolutionScale)
if (current.ps3.msaaMode != base.ps3.msaaMode) j.put("ps3MsaaMode", current.ps3.msaaMode)
if (current.ps3.audioCubebBackend != base.ps3.audioCubebBackend) j.put("ps3AudioCubebBackend", current.ps3.audioCubebBackend)
if (current.ps3.shaderMode != base.ps3.shaderMode) j.put("ps3ShaderMode", current.ps3.shaderMode)
if (current.ps3.frameGeneration != base.ps3.frameGeneration) j.put("ps3FrameGeneration", current.ps3.frameGeneration)
if (current.ps3.frameGenPerformance != base.ps3.frameGenPerformance) j.put("ps3FrameGenPerformance", current.ps3.frameGenPerformance)
if (current.ps3.frameGenFlowScale != base.ps3.frameGenFlowScale) j.put("ps3FrameGenFlowScale", current.ps3.frameGenFlowScale)
if (current.ps3.writeColorBuffers != base.ps3.writeColorBuffers) j.put("ps3WriteColorBuffers", current.ps3.writeColorBuffers)
if (current.ps3.gpuTurbo != base.ps3.gpuTurbo) j.put("ps3GpuTurbo", current.ps3.gpuTurbo)
if (current.ps3.silenceAllLogs != base.ps3.silenceAllLogs) j.put("ps3SilenceAllLogs", current.ps3.silenceAllLogs)
if (current.ps3.writeDepthBuffer != base.ps3.writeDepthBuffer) j.put("ps3WriteDepthBuffer", current.ps3.writeDepthBuffer)
if (current.ps3.readColorBuffers != base.ps3.readColorBuffers) j.put("ps3ReadColorBuffers", current.ps3.readColorBuffers)
if (current.ps3.readDepthBuffer != base.ps3.readDepthBuffer) j.put("ps3ReadDepthBuffer", current.ps3.readDepthBuffer)
@@ -2667,6 +2725,7 @@ data class Settings(
if (current.ps3.netEnabled != base.ps3.netEnabled) j.put("ps3NetEnabled", current.ps3.netEnabled)
if (current.ps3.psnStatus != base.ps3.psnStatus) j.put("ps3PsnStatus", current.ps3.psnStatus)
if (current.ps3.upnpEnabled != base.ps3.upnpEnabled) j.put("ps3UpnpEnabled", current.ps3.upnpEnabled)
if (current.ps3.enterButtonAssign != base.ps3.enterButtonAssign) j.put("ps3EnterButtonAssign", current.ps3.enterButtonAssign)
if (current.ps3.spuXFloat != base.ps3.spuXFloat) j.put("ps3SpuXFloat", current.ps3.spuXFloat)
if (current.ps3.accurateSpuRsv != base.ps3.accurateSpuRsv) j.put("ps3AccurateSpuRsv", current.ps3.accurateSpuRsv)
if (current.ps3.accurateCacheLine != base.ps3.accurateCacheLine) j.put("ps3AccurateCacheLine", current.ps3.accurateCacheLine)
@@ -2940,8 +2999,14 @@ data class Settings(
clocksScale = if (overrides.has("ps3ClocksScale")) overrides.getInt("ps3ClocksScale") else base.ps3.clocksScale,
resolutionScale = if (overrides.has("ps3ResolutionScale")) overrides.getInt("ps3ResolutionScale") else base.ps3.resolutionScale,
msaaMode = if (overrides.has("ps3MsaaMode")) overrides.getInt("ps3MsaaMode") else base.ps3.msaaMode,
audioCubebBackend = if (overrides.has("ps3AudioCubebBackend")) overrides.getInt("ps3AudioCubebBackend") else base.ps3.audioCubebBackend,
shaderMode = if (overrides.has("ps3ShaderMode")) overrides.getInt("ps3ShaderMode") else base.ps3.shaderMode,
frameGeneration = if (overrides.has("ps3FrameGeneration")) overrides.getInt("ps3FrameGeneration") else base.ps3.frameGeneration,
frameGenPerformance = if (overrides.has("ps3FrameGenPerformance")) overrides.getBoolean("ps3FrameGenPerformance") else base.ps3.frameGenPerformance,
frameGenFlowScale = if (overrides.has("ps3FrameGenFlowScale")) overrides.getInt("ps3FrameGenFlowScale") else base.ps3.frameGenFlowScale,
writeColorBuffers = if (overrides.has("ps3WriteColorBuffers")) overrides.getBoolean("ps3WriteColorBuffers") else base.ps3.writeColorBuffers,
gpuTurbo = if (overrides.has("ps3GpuTurbo")) overrides.getBoolean("ps3GpuTurbo") else base.ps3.gpuTurbo,
silenceAllLogs = if (overrides.has("ps3SilenceAllLogs")) overrides.getBoolean("ps3SilenceAllLogs") else base.ps3.silenceAllLogs,
writeDepthBuffer = if (overrides.has("ps3WriteDepthBuffer")) overrides.getBoolean("ps3WriteDepthBuffer") else base.ps3.writeDepthBuffer,
readColorBuffers = if (overrides.has("ps3ReadColorBuffers")) overrides.getBoolean("ps3ReadColorBuffers") else base.ps3.readColorBuffers,
readDepthBuffer = if (overrides.has("ps3ReadDepthBuffer")) overrides.getBoolean("ps3ReadDepthBuffer") else base.ps3.readDepthBuffer,
@@ -2963,6 +3028,7 @@ data class Settings(
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
enterButtonAssign = if (overrides.has("ps3EnterButtonAssign")) overrides.getInt("ps3EnterButtonAssign") else base.ps3.enterButtonAssign,
spuXFloat = if (overrides.has("ps3SpuXFloat")) overrides.getInt("ps3SpuXFloat") else base.ps3.spuXFloat,
accurateSpuRsv = if (overrides.has("ps3AccurateSpuRsv")) overrides.getBoolean("ps3AccurateSpuRsv") else base.ps3.accurateSpuRsv,
accurateCacheLine = if (overrides.has("ps3AccurateCacheLine")) overrides.getBoolean("ps3AccurateCacheLine") else base.ps3.accurateCacheLine,
@@ -468,6 +468,8 @@ val EN: Map<String, String> = mapOf(
"audio.synchronization.label" to "Time Stretching",
"audio.synchronization.description" to "Keeps audio pitch correct when the emulator runs below full speed, instead of letting sound slow and drop in pitch along with it. Costs a little CPU; turn it off if audio stutters more with it on.",
"audio.buffer.label" to "Audio Buffer",
"audio.cubebBackend.label" to "Audio Backend (advanced)",
"audio.cubebBackend.description" to "Which system audio path the emulator uses. Auto picks AAudio, which has the lowest latency but the smallest buffers. OpenSL adds latency and is much harder to starve — try it if audio crackles or stutters on a slower device.",
"audio.buffer.description" to "Bigger buffer = fewer crackles/dropouts but more latency. Raise this if audio stutters on low-end devices.",
// --- Recompiler (JIT) tab ---
"jit.recompiler.warning" to "Disabling a recompiler drops that CPU/COP onto its interpreter — much slower, for debugging only. Changes apply to the running game.",
@@ -523,8 +525,18 @@ val EN: Map<String, String> = mapOf(
"adv.accurateRsxRsv.description" to "Synchronises GPU access to reserved memory strictly. Fixes rare graphical corruption at a performance cost.",
"adv.ppuRsvPriority.label" to "PPU Reservation Priority",
"adv.ppuRsvPriority.description" to "Gives the main CPU priority over the SPUs when competing for the same memory. Can help games that stall waiting on the PPU.",
"pad.section.enterButton" to "Enter Button Assignment",
"pad.enterButton.label" to "Confirm button",
"pad.enterButton.circle" to "Enter with circle",
"pad.enterButton.cross" to "Enter with cross",
"pad.enterButton.description" to "Which button confirms in PS3 system dialogs. Japanese games usually expect circle; most others use cross.",
"app.resetAll" to "Reset all settings",
"app.resetAll.desc" to "Put every setting back to its default. Per-game settings and controller binds are kept.",
"app.resetAll.confirm" to "Every global setting goes back to its default. Per-game overrides and controller binds are not touched.",
"adv.spuVerification.label" to "SPU Verification",
"adv.spuVerification.description" to "Verifies compiled SPU code against the original. Catches miscompiles; turning it off is faster but makes bad codegen silent.",
"adv.preciseSpuVerification.label" to "Precise SPU Verification",
"adv.preciseSpuVerification.description" to "Compares cached SPU code byte for byte instead of by checksum. Slower, but games stream different SPU jobs through the same memory, so a checksum collision can run one job's code on another's data. Try this for physics or logic that goes wrong without crashing, such as falling through the ground.",
"adv.section.fpu" to "Floating Point",
"adv.section.fpu.help" to "PowerPC floating-point corner cases. Defaults are correct for almost every game \u2014 these exist for the handful that depend on exact NaN or rounding behaviour.",
"adv.ppuNan.label" to "PPU Vector NaN Handling",
@@ -596,15 +608,21 @@ val EN: Map<String, String> = mapOf(
"renderer.consoleAspect.description" to "The aspect the emulated PS3 reports to the game. The console only ever signalled 4:3 or 16:9, so those are the only real options \u2014 Auto leaves it to the game. This is what the game renders for; how it is fitted to YOUR screen is the setting below.",
"renderer.fit.auto" to "Fit",
"renderer.fit.stretch" to "Stretch",
"perf.silenceLogs.label" to "Silence all logs",
"perf.silenceLogs.description" to "\u26a0 We cannot help you with a bug if logs are disabled \u2014 a report without a log is usually unfixable. That said, a few games log so heavily that this is worth several FPS, so it can be the difference for a game sitting just under playable. Turn it back on before reporting anything.",
"renderer.gpuTurbo.label" to "GPU Turbo (Adreno)",
"renderer.gpuTurbo.description" to "Pin the GPU to its maximum clocks instead of letting it scale on demand. Removes the stutter when a scene suddenly gets heavy, at the cost of noticeably more heat and battery. Adreno only \u2014 ignored on other GPUs.",
"renderer.upscale.label" to "Internal Resolution",
"audio.renderer.label" to "Audio Backend",
"audio.renderer.description" to "Cubeb is the only working backend on Android. Off disables audio entirely, which can claw back a little CPU on a struggling device.",
"audio.renderer.description" to "Cubeb is the general-purpose backend. Oboe is Android-specific and carries per-device workarounds plus automatic recovery when the audio route changes, so try it if you get crackling or dropouts. Off disables audio entirely, which claws back a little CPU on a struggling device.",
"patches.ps3.header" to "Community patches and graphics mods from RPCS3. These can unlock frame rates, change resolution or LOD, add widescreen, and fix game bugs. Download once, then enable per game.",
"patches.ps3.download" to "Download / update patch database",
"patches.ps3.search" to "Search patches",
"patches.ps3.showing" to "%1 games, %2 patches \u2014 tap a game to see its patches, or search.",
"patches.ps3.downloading" to "Downloading patches\u2026",
"patches.ps3.imported" to "patches imported",
"patches.ps3.importFile" to "Import a patch file",
"patches.ps3.importFailed" to "Could not read that file. Pick a patch.yml exported from RPCS3.",
"patches.ps3.downloadFailed" to "Could not download the patch database. Check your connection.",
"patches.ps3.serverError" to "RPCS3's patch server returned an error.",
"patches.ps3.parseFailed" to "The patch database downloaded but could not be read. It may be for a newer patch format than this build supports.",
@@ -693,6 +711,7 @@ val EN: Map<String, String> = mapOf(
"overlay.uiSize.description" to "Scales menu/library padding and control sizes. 100% = default.",
"overlay.osdColor.label" to "OSD Color",
"overlay.osdColor.description" to "Color of the on-screen display text (FPS, stats and notifications). Speed warnings stay red/green so they still stand out.",
"overlay.osdColor.custom" to "Custom",
"overlay.osdColor.default" to "White",
"overlay.osdColor.green" to "Green",
"overlay.osdColor.cyan" to "Cyan",
@@ -811,6 +830,8 @@ val EN: Map<String, String> = mapOf(
"pad.stickFeel.acceleration.description" to "Non-linear response curve: small tilts stay precise for aiming, full tilt ramps up to full speed. 0 = linear (off); higher = more curve.",
"pad.stickFeel.acceleration.label" to "Acceleration",
"pad.stickFeel.antiDeadzone.description" to "Smallest output sent to the game, to cancel a game's OWN built-in stick deadzone (e.g. Cold Fear / Area 51 ignore the stick until ~45%, then aim jumps). Set near the game's deadzone so any stick movement responds immediately and the full travel maps smoothly above it. 0 = off.",
"pad.stickFeel.squareGate.label" to "Full Diagonal Range",
"pad.stickFeel.squareGate.description" to "Sends the full range on diagonals instead of the reduced value a real DualShock gives (~70%), so diagonal movement is as fast as straight up/down/left/right. On by default. Turn off to match original hardware exactly.",
"pad.stickFeel.antiDeadzone.label" to "Anti-Deadzone",
"pad.stickFeel.deadzone.description" to "Fraction of physical analog travel ignored near center (applied to the stick's radial distance, so diagonals behave like cardinals). Output re-normalizes past it, so movement still ramps smoothly from 0 — which also means the on-screen effect can be masked by a game's OWN built-in deadzone (Area 51 ignores input below ~45% no matter what you set here; use Anti-Deadzone for that). 0 = off — raw hardware values pass through, including any stick drift.",
"pad.stickFeel.deadzone.label" to "Deadzone",
@@ -1029,6 +1050,22 @@ val EN: Map<String, String> = mapOf(
"perf.decoder.interpreterDyn" to "Interpreter (dyn)",
"perf.decoder.asmjit" to "ASMJIT",
"perf.decoder.llvm" to "LLVM",
"perf.framegen.title" to "Frame Generation (Experimental)",
"perf.framegen.import" to "Import from Lossless Scaling\u2026",
"perf.framegen.import.missing" to "Shaders not imported \u2014 frame generation will not run.",
"perf.framegen.import.ok" to "Shaders imported (%d).",
"perf.framegen.import.working" to "Reading shaders\u2026",
"perf.framegen.import.failed" to "Could not read shaders from that file.",
"perf.framegen.label" to "Lossless Scaling",
"perf.framegen.performance.label" to "Performance shaders",
"perf.framegen.performance.description" to "Use Lossless Scaling's lighter 3.1p shaders instead of the full-quality 3.1 set. Cheaper to run and slightly softer in motion \u2014 on by default, because the quality set usually costs more than the frames it buys on a phone. Both come from the file you imported, so switching does not need another import.\n\nTakes effect when frame generation next starts: turn it off and on again, or restart the game.",
"perf.framegen.flowScale.label" to "Motion detail",
"perf.framegen.flowScale.description" to "How finely motion is measured between frames, as a percentage of full resolution. Lower is faster and blurrier around moving edges. Drop this before dropping the multiplier if frame generation is costing more than it gives.\n\nTakes effect when frame generation next starts.",
"perf.framegen.off" to "Off",
"perf.framegen.x2" to "x2",
"perf.framegen.x3" to "x3",
"perf.framegen.x4" to "x4",
"perf.framegen.description" to "EXPERIMENTAL. Insert generated frames between the ones the game actually draws. Costs GPU time and adds latency, so it helps when the CPU is the limit and hurts when the GPU already is.\n\nIt works best from a steady framerate. Interpolating a game that is already struggling tends to look worse rather than better \u2014 generated frames land at the wrong moment when the real interval keeps changing, which reads as judder. A locked 25 usually looks better than a wandering 28.\n\nOn-screen text shimmers or flickers while this is on \u2014 the overlay and the game\u0027s own menus get interpolated along with everything else, and fine text is what that looks worst on. That is how frame generation behaves, not a fault. Turning it off restores steady text. Switching it on or off during a game also pauses for a few seconds while the shaders are prepared.\n\nThis does nothing until you import Lossless.dll below. It is part of Lossless Scaling on Steam \u2014 you need your own copy, and nothing is bundled or downloaded. On Windows the file sits in steamapps\\common\\Lossless Scaling\\Lossless.dll; copy it to your device and pick it with the button below. Only the shaders are kept, and your copy of the file is deleted afterwards.",
"perf.ppuDecoder.label" to "PPU Decoder",
"perf.ppuDecoder.description" to "How the PS3's main CPU (PPU) is executed. LLVM recompiles PowerPC to native ARM64 and is enormously faster \u2014 keep it unless you are debugging. Interpreter is only for diagnosing a game LLVM gets wrong.",
"perf.spuDecoder.label" to "SPU Decoder",
@@ -364,6 +364,22 @@ object ControllerMappings {
private const val KEY_STICK_ANTIDZ = "pad.stick.antiDeadzone"
const val STICK_ANTIDZ_MAX = 0.60f
private val prefStickAntiDz = PerStickPref(KEY_STICK_ANTIDZ, 0.0f, 0f, STICK_ANTIDZ_MAX)
// Square gate: send the full per-axis range on diagonals instead of capping them to the
// unit circle. A DualShock 3 is circular-gated, so a full diagonal is ~0.707 per axis, and
// emitting that is technically faithful -- but it lands inside the internal deadzone of games
// that test each axis separately, and their camera then crawls diagonally while the cardinals
// are fine. Oblivion is the case that found this.
//
// DEFAULT ON. Faithfulness to a circular gate is not worth a control scheme that feels broken,
// and a modern pad's own gate is closer to square anyway. Off restores the hardware curve for
// anyone who wants it. Per stick.
private const val KEY_STICK_SQUARE = "pad.stick.square"
fun stickSquareGate(left: Boolean): Boolean =
MainActivityRuntime.prefs.getBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", true)
fun setStickSquareGate(left: Boolean, v: Boolean) =
MainActivityRuntime.prefs.edit { putBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", v) }
fun stickAntiDeadzone(left: Boolean): Float = prefStickAntiDz.get(left)
fun setStickAntiDeadzone(left: Boolean, v: Float) = prefStickAntiDz.set(left, v)

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