mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b45eee27 | ||
|
|
87ccdb8515 |
@@ -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
|
||||
|
||||
Vendored
+6
@@ -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)
|
||||
|
||||
|
||||
Vendored
+8
@@ -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)
|
||||
+1
Submodule 3rdparty/oboe/oboe added at 0da326e4ef
+34
-3
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}();
|
||||
|
||||
|
||||
@@ -2746,6 +2746,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();
|
||||
|
||||
@@ -29,8 +29,8 @@ android {
|
||||
applicationId = "com.armsx3"
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
versionCode = 10
|
||||
versionName = "0.6"
|
||||
versionCode = 11
|
||||
versionName = "0.7"
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -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);
|
||||
@@ -127,6 +130,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"));
|
||||
@@ -614,6 +620,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 +985,29 @@ 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());
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -176,6 +176,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,
|
||||
@@ -1017,6 +1027,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())
|
||||
@@ -1049,6 +1060,7 @@ 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())
|
||||
@@ -1994,8 +2006,11 @@ data class Settings(
|
||||
put("ps3ClocksScale", ps3.clocksScale)
|
||||
put("ps3ResolutionScale", ps3.resolutionScale)
|
||||
put("ps3MsaaMode", ps3.msaaMode)
|
||||
put("ps3AudioCubebBackend", ps3.audioCubebBackend)
|
||||
put("ps3ShaderMode", ps3.shaderMode)
|
||||
put("ps3WriteColorBuffers", ps3.writeColorBuffers)
|
||||
put("ps3GpuTurbo", ps3.gpuTurbo)
|
||||
put("ps3SilenceAllLogs", ps3.silenceAllLogs)
|
||||
put("ps3WriteDepthBuffer", ps3.writeDepthBuffer)
|
||||
put("ps3ReadColorBuffers", ps3.readColorBuffers)
|
||||
put("ps3ReadDepthBuffer", ps3.readDepthBuffer)
|
||||
@@ -2329,8 +2344,11 @@ 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),
|
||||
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),
|
||||
@@ -2644,8 +2662,11 @@ 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.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)
|
||||
@@ -2940,8 +2961,11 @@ 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,
|
||||
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,
|
||||
|
||||
@@ -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.",
|
||||
@@ -525,6 +527,8 @@ val EN: Map<String, String> = mapOf(
|
||||
"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.",
|
||||
"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 +600,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.",
|
||||
|
||||
@@ -813,6 +813,15 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
upscale.value = resolved.upscaleFloat
|
||||
renderer.value = resolved.renderer
|
||||
|
||||
// GPU turbo, resolved per game like the driver below it. Applied here rather than
|
||||
// through the config tree because it is not an emulator setting at all -- it is a KGSL
|
||||
// ioctl straight to the Adreno kernel driver, so nothing in g_cfg would carry it.
|
||||
//
|
||||
// Safe unconditionally: adrenotools no-ops on non-Adreno hardware and if /dev/kgsl-3d0
|
||||
// cannot be opened. Re-applied on every start so turning it off actually releases the
|
||||
// clocks on the next boot rather than persisting until reboot.
|
||||
runCatching { net.rpcsx.RPCSX.instance.setGpuTurbo(resolved.ps3.gpuTurbo) }
|
||||
|
||||
NativeApp.renderUpscalemultiplier(upscale.value)
|
||||
// Pin custom Vulkan driver (if any) BEFORE the renderer write —
|
||||
// the renderer JNI may trigger MTGS::ApplySettings which can
|
||||
@@ -1772,12 +1781,38 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
// reinstalling clean (#376/#385). The cache is pure derived data (rebuilt on demand),
|
||||
// never user content, so wiping it is always safe. Skipped on first install (no prior
|
||||
// version recorded) — there is nothing stale to clear.
|
||||
// ONLY the GPU caches, never the compiled guest modules.
|
||||
//
|
||||
// This came from ARMSX2, where <dataRoot>/cache held the GS shader/pipeline cache and
|
||||
// nothing else, so deleting the lot was free. In ARMSX3 that same directory is RPCS3's
|
||||
// whole cache root: <dataRoot>/cache/cache/<TITLEID>/ppu-<hash>-EBOOT.BIN/ holds every
|
||||
// compiled PPU module, and deleting it threw away work measured in tens of minutes per
|
||||
// game -- an hour for the XMB's 390 firmware modules. Every update, on purpose, by code
|
||||
// that believed it was clearing shaders. That is the "why do I have to recompile my games
|
||||
// after every update" report, and it is the single worst thing about updating.
|
||||
//
|
||||
// The original worry stands and is preserved: a pipeline cache baked against a different
|
||||
// core build can render corrupt (#376/#385). But that argument is about GPU pipeline blobs,
|
||||
// not guest code. PPU objects already carry their own compatibility key in the filename --
|
||||
// format version, module hash, the settings that affect codegen, and the CPU target -- so a
|
||||
// build that changes any of that simply does not match them, and one that does not change
|
||||
// it has no reason to discard them.
|
||||
runCatching {
|
||||
val prevVc = prefs.getInt("lastRunVersionCode", 0)
|
||||
val curVc = BuildConfig.VERSION_CODE
|
||||
if (prevVc != 0 && prevVc != curVc) {
|
||||
File(assetCopyRoot(applicationContext), "cache").deleteRecursively()
|
||||
android.util.Log.i("ARMSX2", "Update $prevVc -> $curVc: cleared GS shader/pipeline cache")
|
||||
val root = File(assetCopyRoot(applicationContext), "cache")
|
||||
var cleared = 0
|
||||
// Depth-first over the cache root, removing only directories named shaders_cache
|
||||
// (RPCS3 puts one beside each title's compiled modules). walkBottomUp so a match is
|
||||
// deleted whole without the walk then descending into a directory that is gone.
|
||||
root.walkBottomUp()
|
||||
.filter { it.isDirectory && it.name == "shaders_cache" }
|
||||
.forEach { if (it.deleteRecursively()) cleared++ }
|
||||
android.util.Log.i(
|
||||
"ARMSX2",
|
||||
"Update $prevVc -> $curVc: cleared $cleared shader cache(s); compiled modules kept",
|
||||
)
|
||||
}
|
||||
if (prevVc != curVc) prefs.edit { putInt("lastRunVersionCode", curVc) }
|
||||
}
|
||||
|
||||
@@ -877,6 +877,14 @@ private fun GraphicsPane(state: EmulationMenuUiState, viewModel: EmulationMenuVi
|
||||
MenuSwitchRow(str("renderer.readColorBuffers.label"), settings.ps3.readColorBuffers) { v ->
|
||||
viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(readColorBuffers = v)) }
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
// GPU Turbo in-game: the whole point is A/B-ing it against a scene that is actually
|
||||
// stuttering, which is impossible if you have to quit to Settings to flip it. Applied
|
||||
// live, not just at the next renderer start.
|
||||
MenuSwitchRow(str("renderer.gpuTurbo.label"), settings.ps3.gpuTurbo) { v ->
|
||||
viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(gpuTurbo = v)) }
|
||||
runCatching { net.rpcsx.RPCSX.instance.setGpuTurbo(v) }
|
||||
}
|
||||
// Overlay artwork, switchable from in-game — trying bezels means seeing them ON the game, and
|
||||
// having to leave for All Settings each time made that unusable. Import still lives in the
|
||||
// settings tab (it opens a file picker); this is the picker for what is already imported.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.armsx2.ui.patches
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -25,6 +27,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.armsx2.Ps3PatchRepo
|
||||
@@ -79,6 +82,30 @@ fun Ps3PatchesTab(serial: String = "") {
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
// No MIME filter: patch.yml is served as text/plain, application/octet-stream or
|
||||
// nothing at all depending on where it came from, and filtering hides the file
|
||||
// the user is looking straight at.
|
||||
val patchPicker = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
busy = true
|
||||
message = null
|
||||
scope.launch {
|
||||
val r = withContext(Dispatchers.IO) { Ps3PatchRepo.importLocal(context, uri) }
|
||||
busy = false
|
||||
message = when (r) {
|
||||
is Ps3PatchRepo.Result.Ok -> {
|
||||
reload()
|
||||
"${r.count} " + str2("patches.ps3.imported")
|
||||
}
|
||||
Ps3PatchRepo.Result.Parse -> str2("patches.ps3.parseFailed")
|
||||
else -> str2("patches.ps3.importFailed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(serial) { reload() }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
@@ -119,6 +146,17 @@ fun Ps3PatchesTab(serial: String = "") {
|
||||
Text(str("patches.ps3.download"))
|
||||
}
|
||||
|
||||
// Local import sits next to the download because the two produce the same
|
||||
// result -- patchesImport merges either source into patches/patch.yml.
|
||||
OutlinedButton(
|
||||
onClick = { if (!busy) patchPicker.launch(arrayOf("*/*")) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.controllerFocusable("patches.import", RoundedCornerShape(13.dp)),
|
||||
) {
|
||||
Text(str("patches.ps3.importFile"))
|
||||
}
|
||||
|
||||
if (busy) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@@ -71,12 +71,14 @@ fun AudioTab(state: MutableState<Settings>) {
|
||||
// Audio backend. Only Cubeb and Null are real on Android -- XAudio2 is
|
||||
// Windows-only and FAudio is not built here -- so offering the other two
|
||||
// would just be a way to silently kill audio.
|
||||
// Indices here are positions in Rpcs3Settings.AUDIO_RENDERERS: Cubeb=2, Oboe=4, Null=0.
|
||||
val rendererIndices = listOf(2, 4, 0)
|
||||
SegmentedRow(
|
||||
label = str("audio.renderer.label"),
|
||||
options = listOf("Cubeb", str("common.off")),
|
||||
selectedIndex = if (s.ps3.audioRenderer == 0) 1 else 0,
|
||||
options = listOf("Cubeb", "Oboe", str("common.off")),
|
||||
selectedIndex = rendererIndices.indexOf(s.ps3.audioRenderer).coerceAtLeast(0),
|
||||
description = str("audio.renderer.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(audioRenderer = if (it == 0) 2 else 0))) },
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(audioRenderer = rendererIndices[it]))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SegmentedGridRow(
|
||||
@@ -100,8 +102,25 @@ fun AudioTab(state: MutableState<Settings>) {
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(audioChannels = intArrayOf(0, 1, 2, 6, 7)[it]))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
// The replacement for PCSX2's OpenSL ES toggle, which was removed from here on the
|
||||
// reasoning that "RPCS3 picks its backend via Audio Renderer". That is not the same knob:
|
||||
// Audio Renderer chooses Cubeb or Null, while THIS chooses which backend cubeb then talks
|
||||
// to. Android builds all three, and cubeb's auto order takes AAudio on anything modern, so
|
||||
// OpenSL had quietly become unreachable rather than superseded.
|
||||
//
|
||||
// It matters because AAudio's low-latency path takes the smallest buffers the device will
|
||||
// grant, and those are the first thing to underrun once the emulator drops below full
|
||||
// speed -- the audio stutter reported on low-end Mali devices, which faster hardware never
|
||||
// shows. OpenSL trades latency for buffers that are much harder to starve.
|
||||
SegmentedGridRow(
|
||||
label = str("audio.cubebBackend.label"),
|
||||
options = listOf(str("common.auto"), "AAudio", "OpenSL", "AudioTrack"),
|
||||
selectedIndex = s.ps3.audioCubebBackend.coerceIn(0, 3),
|
||||
columns = 4,
|
||||
description = str("audio.cubebBackend.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(audioCubebBackend = it))) },
|
||||
)
|
||||
// Removed: SPU2 is the PS2's sound chip; its NEON reverb path does not exist here.
|
||||
// Removed: PCSX2's OpenSL ES toggle. RPCS3 picks its backend via Audio Renderer (Cubeb on Android).
|
||||
// Removed: PCSX2 SPU2 lightweight mixing mode. No RPCS3 counterpart.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@ fun FixesTab(state: MutableState<Settings>) {
|
||||
s.ps3.spuVerification,
|
||||
description = str("adv.spuVerification.description"),
|
||||
) { apply(s.copy(ps3 = s.ps3.copy(spuVerification = it))) }
|
||||
SettingsDivider()
|
||||
// Distinct from the toggle above: that one chooses whether to verify at all, this one
|
||||
// chooses how. The field was already serialised and written to the config tree but had
|
||||
// no control, so the only way to reach it was a raw core override.
|
||||
ToggleRow(
|
||||
str("adv.preciseSpuVerification.label"),
|
||||
s.ps3.preciseSpuVerification,
|
||||
description = str("adv.preciseSpuVerification.description"),
|
||||
) { apply(s.copy(ps3 = s.ps3.copy(preciseSpuVerification = it))) }
|
||||
}
|
||||
|
||||
CollapsibleSection(str("adv.section.fpu")) {
|
||||
|
||||
@@ -192,6 +192,40 @@ fun PerformanceTab(state: MutableState<Settings>) {
|
||||
)
|
||||
}
|
||||
SettingsDivider()
|
||||
// ---- GPU Turbo (Adreno) -------------------------------------------------
|
||||
// A KGSL ioctl that pins the GPU to maximum clocks instead of letting DVFS ramp. Lives
|
||||
// here rather than with the driver picker because what it actually trades is heat and
|
||||
// battery for frametime stability -- that is a performance decision, not a display one.
|
||||
SegmentedRow(
|
||||
label = str("renderer.gpuTurbo.label"),
|
||||
options = listOf(str("common.off"), str("common.on")),
|
||||
selectedIndex = if (s.ps3.gpuTurbo) 1 else 0,
|
||||
description = str("renderer.gpuTurbo.description"),
|
||||
onChange = {
|
||||
val on = it == 1
|
||||
apply(s.copy(ps3 = s.ps3.copy(gpuTurbo = on)))
|
||||
// Apply live as well as at the next renderer start, so it is testable without
|
||||
// rebooting the game.
|
||||
runCatching { net.rpcsx.RPCSX.instance.setGpuTurbo(on) }
|
||||
},
|
||||
)
|
||||
SettingsDivider()
|
||||
// ---- Silence all logs ---------------------------------------------------
|
||||
// A genuine performance lever, not a debug knob: a few games log thousands of lines a
|
||||
// second and every one costs a write. Measured at 5-7 FPS on LittleBigPlanet 2, which
|
||||
// can be the difference between unplayable and playable.
|
||||
//
|
||||
// Default OFF, deliberately. A bug report without a log is usually unfixable, and the
|
||||
// description says so in as many words -- the setting is here for people who already
|
||||
// know the trade and want the frames.
|
||||
SegmentedRow(
|
||||
label = str("perf.silenceLogs.label"),
|
||||
options = listOf(str("common.off"), str("common.on")),
|
||||
selectedIndex = if (s.ps3.silenceAllLogs) 1 else 0,
|
||||
description = str("perf.silenceLogs.description"),
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(silenceAllLogs = it == 1))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
// Affinity Control Mode — opt-in CPU pinning for the EE/VU/GS threads. Android normally
|
||||
// leaves them unpinned on purpose (EAS puts the busiest thread on the prime core, and
|
||||
// pinning VU to a mid-tier big core measured ~1.4x slower), so this is EXPERIMENTAL and
|
||||
|
||||
@@ -53,6 +53,11 @@ fun RendererBackendSection(state: MutableState<Settings>) {
|
||||
}
|
||||
}
|
||||
|
||||
// GPU Turbo used to live here, on the reasoning that it belongs with the driver. It does
|
||||
// technically -- it is a KGSL ioctl, not an emulator setting -- but the driver picker sits
|
||||
// inside "Display & Resolution", so the net effect was a power/thermal lever filed under
|
||||
// display options where nobody could find it. It is on the Performance tab now.
|
||||
|
||||
SettingsDivider()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
|
||||
@@ -73,6 +73,8 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("renderer.section.overlayArt", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.section.shaderChain", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.section.rsxAccuracy", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.gpuTurbo.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("perf.silenceLogs.label", true, SettingsCategory.Performance),
|
||||
SettingsSearchEntry("renderer.writeColorBuffers.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.writeDepthBuffer.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.readColorBuffers.label", true, SettingsCategory.Graphics),
|
||||
@@ -93,6 +95,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("audio.synchronization.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("audio.buffer.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("audio.renderer.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("audio.cubebBackend.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("audio.format.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("audio.channels.label", true, SettingsCategory.Audio),
|
||||
SettingsSearchEntry("pad.section.playerRumble", true, SettingsCategory.Controls),
|
||||
@@ -163,6 +166,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("adv.accurateRsxRsv.label", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.ppuRsvPriority.label", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.spuVerification.label", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.preciseSpuVerification.label", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.section.fpu", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.ppuNan.label", true, SettingsCategory.Advanced),
|
||||
SettingsSearchEntry("adv.accurateDfma.label", true, SettingsCategory.Advanced),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user