mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33343cd153 | ||
|
|
6cd7866553 | ||
|
|
6a5ad70ec7 | ||
|
|
c990f34d5f | ||
|
|
bbaebe47a4 | ||
|
|
e480c291da | ||
|
|
7d25a7086e | ||
|
|
dbbb6fbde0 | ||
|
|
d069a55acc | ||
|
|
614bf8b718 | ||
|
|
cce09dbb39 | ||
|
|
b5a715adcf | ||
|
|
eb54f9b75a | ||
|
|
0a9fd15b57 | ||
|
|
0821bbf956 | ||
|
|
b29810d1a5 | ||
|
|
5ef731c9e5 | ||
|
|
002a9b274a | ||
|
|
39ca5cdab6 | ||
|
|
b82432c793 | ||
|
|
7f54855b7d | ||
|
|
0819f1ef15 | ||
|
|
8ee20d91d5 | ||
|
|
4797ad8a9a | ||
|
|
c4b45eee27 | ||
|
|
87ccdb8515 | ||
|
|
e10f846924 | ||
|
|
db6ee86806 | ||
|
|
708582e523 | ||
|
|
8a6deab362 | ||
|
|
5b740f8921 | ||
|
|
11f043b529 | ||
|
|
b8987b8c92 | ||
|
|
403df1c651 | ||
|
|
614cdd74a3 | ||
|
|
76af990eed | ||
|
|
441c3f1bde | ||
|
|
fcdd7cd6af | ||
|
|
ad15f81d6e | ||
|
|
2df75aa604 | ||
|
|
b0c7a0260e | ||
|
|
7745a3c92d | ||
|
|
b09815e595 | ||
|
|
3791865e2f | ||
|
|
6831c87a0e | ||
|
|
b4378d8977 | ||
|
|
8fed09d40e | ||
|
|
8772786f9a | ||
|
|
5405d71e2e | ||
|
|
4101367b2d | ||
|
|
466d85d5b9 |
@@ -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
+15
@@ -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)
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Vendored
+147
@@ -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")
|
||||
Vendored
+32
@@ -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:
|
||||
*;
|
||||
};
|
||||
Vendored
+404
@@ -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
|
||||
Vendored
+142
@@ -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
|
||||
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
@@ -1,39 +1,8 @@
|
||||
ARMSX3
|
||||
======
|
||||
|
||||
Proof of concept Android port of RPCS3.
|
||||
|
||||
Uses the latest RPCS3 upstream code (the recent ARM64 improvements included).
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
From my testing, I only tried Skate 3. It boots, loads and reaches gameplay at roughly 20 to 30 fps on a
|
||||
Snapdragon 8 Gen 2. Rendering, audio, touch controls and physical controllers
|
||||
work. Almost nothing else has been tested. So the main stop gap at the moment is performance/speed.
|
||||
|
||||
Differences from upstream RPCS3
|
||||
-------------------------------
|
||||
|
||||
Some of the fixes here are not in upstream and affect any ARM64 build, not only
|
||||
Android:
|
||||
|
||||
* Shaders declared runtime sized arrays inside uniform blocks, which requires
|
||||
VK_EXT_shader_uniform_buffer_unsized_array. Adreno does not support that
|
||||
extension, so every game pipeline failed to compile and nothing rendered.
|
||||
Concrete array bounds are emitted when the extension is missing.
|
||||
|
||||
* The ARM64 SPU block verification checksum folded two thirds of every block
|
||||
through an absolute difference. That collides on the near identical job
|
||||
binaries an SPU job manager streams through the same local store address, so
|
||||
a cached block could end up running against another job's code. It sums now.
|
||||
|
||||
* Thread affinity was compiled out on Android, and the core had no ARM
|
||||
big.LITTLE topology, so SPU and RSX threads were never placed on the fast
|
||||
cores.
|
||||
|
||||
* The LLVM JIT target was pinned to cortex-a34, an in order core from 2016. It
|
||||
detects the host now.
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
+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;
|
||||
}();
|
||||
|
||||
|
||||
+66
-2
@@ -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();
|
||||
|
||||
@@ -87,42 +87,13 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// ARMSX3: fail the build if the bundled ANGLE libraries are not there.
|
||||
//
|
||||
// This check exists because of a specific, expensive bug in ARMSX2: the repo's
|
||||
// blanket `*.so` gitignore rule swallowed the ANGLE prebuilts, they never made it
|
||||
// into release staging, the APK shipped without them, and the core fell back to
|
||||
// the system GLES driver in complete silence. Users reported "ANGLE is broken"
|
||||
// and there was nothing in any log to contradict them.
|
||||
//
|
||||
// jniLibs/.gitignore now un-ignores the two files by name. This task is the
|
||||
// second lock: packaging an APK that claims to support ANGLE without shipping
|
||||
// ANGLE is a build error, not a runtime surprise. The core-side counterpart is
|
||||
// the loud error in gl::es::egl_initialize() when the override library is
|
||||
// selected but cannot be dlopen'd.
|
||||
val verifyAngleLibs by tasks.registering {
|
||||
val angleLibs = listOf("libEGL_angle.so", "libGLESv2_angle.so")
|
||||
val jniLibDir = file("src/main/jniLibs/arm64-v8a")
|
||||
|
||||
doLast {
|
||||
val missing = angleLibs.filter { !File(jniLibDir, it).isFile }
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"ANGLE libraries missing from ${'$'}jniLibDir: ${'$'}{missing.joinToString(", ")}.\n" +
|
||||
"The OpenGL renderer's ANGLE option cannot work without them and would " +
|
||||
"silently fall back to the system GLES driver.\n" +
|
||||
"They are tracked in git - check them out, or remove the ANGLE option."
|
||||
)
|
||||
}
|
||||
|
||||
angleLibs.forEach {
|
||||
logger.lifecycle("ANGLE: packaging ${'$'}it (${'$'}{File(jniLibDir, it).length()} bytes)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }
|
||||
.configureEach { dependsOn(verifyAngleLibs) }
|
||||
// ARMSX3: the ANGLE prebuilts and the verifyAngleLibs task that guarded them used
|
||||
// to live here. They now live in android/armsx3-ui, which is the module that
|
||||
// actually ships (applicationId com.armsx3) and the module whose UI exposes the
|
||||
// OpenGL renderer's ANGLE option. This module builds nothing that ships, so the
|
||||
// guard here could never protect the APK it was written for -- and it never ran at
|
||||
// all: its message was written with `${'$'}`-style template escaping, and the `", "`
|
||||
// inside it closed the Kotlin string early, so this file did not compile.
|
||||
|
||||
base.archivesName = "rpcsx"
|
||||
|
||||
|
||||
@@ -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 = 9
|
||||
versionName = "0.5"
|
||||
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.
|
||||
@@ -110,6 +113,51 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// ARMSX3: fail the build if the bundled ANGLE libraries are not there.
|
||||
//
|
||||
// This check exists because of a specific, expensive bug in ARMSX2: the repo's
|
||||
// blanket `*.so` gitignore rule swallowed the ANGLE prebuilts, they never made it
|
||||
// into release staging, the APK shipped without them, and the core fell back to
|
||||
// the system GLES driver in complete silence. Users reported "ANGLE is broken"
|
||||
// and there was nothing in any log to contradict them.
|
||||
//
|
||||
// jniLibs/.gitignore un-ignores the two files by name. This task is the second
|
||||
// lock: packaging an APK that claims to support ANGLE without shipping ANGLE is a
|
||||
// build error, not a runtime surprise. The core-side counterpart is the loud error
|
||||
// in gl::es::egl_initialize() when the override library is selected but cannot be
|
||||
// dlopen'd; the UI-side counterpart is the MISSING_LIBS line that
|
||||
// MainActivityRuntime.applyAngleEnv logs when the option is on and the .so is not
|
||||
// in nativeLibraryDir.
|
||||
//
|
||||
// The claim being guarded is live in THIS module: RendererBackendSection ->
|
||||
// AngleDriverSection writes Settings.useAngleOpenGL, and applyAngleEnv turns it
|
||||
// into ARMSX2_ANGLE_EGL_LIBRARY. (Both the libraries and this task used to sit in
|
||||
// the stale android/armsx3-app module, which builds nothing that ships -- so the
|
||||
// guard could not fire for the APK it was meant to protect.)
|
||||
val verifyAngleLibs by tasks.registering {
|
||||
val angleLibs = listOf("libEGL_angle.so", "libGLESv2_angle.so")
|
||||
val jniLibDir = file("src/main/jniLibs/arm64-v8a")
|
||||
|
||||
doLast {
|
||||
val missing = angleLibs.filter { !jniLibDir.resolve(it).isFile }
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"ANGLE libraries missing from $jniLibDir: ${missing.joinToString(", ")}.\n" +
|
||||
"The OpenGL renderer's ANGLE option cannot work without them and would " +
|
||||
"silently fall back to the system GLES driver.\n" +
|
||||
"They are tracked in git - check them out, or remove the ANGLE option."
|
||||
)
|
||||
}
|
||||
|
||||
angleLibs.forEach {
|
||||
logger.lifecycle("ANGLE: packaging $it (${jniLibDir.resolve(it).length()} bytes)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") }
|
||||
.configureEach { dependsOn(verifyAngleLibs) }
|
||||
|
||||
dependencies {
|
||||
// Discord Social SDK, staged locally rather than pulled from a repo: it is
|
||||
// proprietary and distributed per-application from the developer portal.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#include <algorithm>
|
||||
#include <android/api-level.h>
|
||||
#include <android/dlext.h>
|
||||
#include <android/log.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <jni.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -19,6 +23,7 @@
|
||||
struct RPCSXApi {
|
||||
bool (*overlayPadData)(int port, int digital1, int digital2, int leftStickX,
|
||||
int leftStickY, int rightStickX, int rightStickY);
|
||||
bool (*overlayPadPressure)(int port, const int *values, int count);
|
||||
bool (*initialize)(std::string_view rootDir, std::string_view user);
|
||||
void (*setSocInfo)(std::string_view socInfo);
|
||||
bool (*processCompilationQueue)(JNIEnv *env);
|
||||
@@ -30,8 +35,13 @@ struct RPCSXApi {
|
||||
int (*getState)();
|
||||
void (*kill)();
|
||||
void (*resume)();
|
||||
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);
|
||||
bool (*usbDeviceEvent)(int fd, int vendorId, int productId, int event);
|
||||
@@ -46,12 +56,16 @@ 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);
|
||||
bool (*uninstallGame)(std::string_view path);
|
||||
std::string (*getVersion)();
|
||||
void *(*setCustomDriver)(void *driverHandle);
|
||||
void (*reportDriverProblem)(std::string message);
|
||||
bool (*saveState)();
|
||||
bool (*loadState)(unsigned int index);
|
||||
bool (*hasState)(unsigned int index);
|
||||
@@ -61,6 +75,7 @@ struct RPCSXApi {
|
||||
bool (*saveStateToSlot)(unsigned int slot);
|
||||
bool (*loadStateFromSlot)(unsigned int slot);
|
||||
bool (*hasStateInSlot)(unsigned int slot);
|
||||
std::string (*patchEngineVersion)();
|
||||
int (*patchesImport)(std::string_view content);
|
||||
std::string (*patchesList)(std::string_view serial);
|
||||
std::string (*probeDiscInfo)(std::string_view isoPath, std::string_view iconOut);
|
||||
@@ -104,6 +119,7 @@ struct RPCSXLibrary : RPCSXApi {
|
||||
|
||||
// clang-format off
|
||||
result.overlayPadData = reinterpret_cast<decltype(overlayPadData)>(dlsym(handle, "_rpcsx_overlayPadData"));
|
||||
result.overlayPadPressure = reinterpret_cast<decltype(overlayPadPressure)>(dlsym(handle, "_rpcsx_overlayPadPressure"));
|
||||
result.initialize = reinterpret_cast<decltype(initialize)>(dlsym(handle, "_rpcsx_initialize"));
|
||||
result.setSocInfo = reinterpret_cast<decltype(setSocInfo)>(dlsym(handle, "_rpcsx_setSocInfo"));
|
||||
result.processCompilationQueue = reinterpret_cast<decltype(processCompilationQueue)>(dlsym(handle, "_rpcsx_processCompilationQueue"));
|
||||
@@ -114,8 +130,13 @@ struct RPCSXLibrary : RPCSXApi {
|
||||
result.getState = reinterpret_cast<decltype(getState)>(dlsym(handle, "_rpcsx_getState"));
|
||||
result.kill = reinterpret_cast<decltype(kill)>(dlsym(handle, "_rpcsx_kill"));
|
||||
result.resume = reinterpret_cast<decltype(resume)>(dlsym(handle, "_rpcsx_resume"));
|
||||
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"));
|
||||
result.usbDeviceEvent = reinterpret_cast<decltype(usbDeviceEvent)>(dlsym(handle, "_rpcsx_usbDeviceEvent"));
|
||||
@@ -129,18 +150,26 @@ 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"));
|
||||
result.uninstallGame = reinterpret_cast<decltype(uninstallGame)>(dlsym(handle, "_rpcsx_uninstallGame"));
|
||||
result.getVersion = reinterpret_cast<decltype(getVersion)>(dlsym(handle, "_rpcsx_getVersion"));
|
||||
result.setCustomDriver = reinterpret_cast<decltype(setCustomDriver)>(dlsym(handle, "_rpcsx_setCustomDriver"));
|
||||
result.reportDriverProblem = reinterpret_cast<decltype(reportDriverProblem)>(dlsym(handle, "_rpcsx_reportDriverProblem"));
|
||||
result.saveState = reinterpret_cast<decltype(saveState)>(dlsym(handle, "_rpcsx_saveState"));
|
||||
result.loadState = reinterpret_cast<decltype(loadState)>(dlsym(handle, "_rpcsx_loadState"));
|
||||
result.hasState = reinterpret_cast<decltype(hasState)>(dlsym(handle, "_rpcsx_hasState"));
|
||||
result.saveStateToSlot = reinterpret_cast<decltype(saveStateToSlot)>(dlsym(handle, "_rpcsx_saveStateToSlot"));
|
||||
result.loadStateFromSlot = reinterpret_cast<decltype(loadStateFromSlot)>(dlsym(handle, "_rpcsx_loadStateFromSlot"));
|
||||
result.hasStateInSlot = reinterpret_cast<decltype(hasStateInSlot)>(dlsym(handle, "_rpcsx_hasStateInSlot"));
|
||||
result.patchEngineVersion = reinterpret_cast<decltype(patchEngineVersion)>(dlsym(handle, "_rpcsx_patchEngineVersion"));
|
||||
result.patchesImport = reinterpret_cast<decltype(patchesImport)>(dlsym(handle, "_rpcsx_patchesImport"));
|
||||
result.patchesList = reinterpret_cast<decltype(patchesList)>(dlsym(handle, "_rpcsx_patchesList"));
|
||||
result.probeDiscInfo = reinterpret_cast<decltype(probeDiscInfo)>(dlsym(handle, "_rpcsx_probeDiscInfo"));
|
||||
@@ -201,6 +230,35 @@ extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadData(
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_overlayPadPressure(
|
||||
JNIEnv *env, jobject, jint port, jintArray values) {
|
||||
// Absent on a core older than this export: the pad still works, every button
|
||||
// is just digital, which is the behaviour that shipped before it existed.
|
||||
if (rpcsxLib.overlayPadPressure == nullptr || values == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const jsize count = env->GetArrayLength(values);
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Critical rather than a copy: this runs on every input event that moves a
|
||||
// trigger, and the callee only reads the values before returning.
|
||||
auto *elems = static_cast<jint *>(env->GetPrimitiveArrayCritical(values, nullptr));
|
||||
if (elems == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(sizeof(jint) == sizeof(int),
|
||||
"jint and int must match for the pressure array to be passed through");
|
||||
const bool ok = rpcsxLib.overlayPadPressure(
|
||||
port, reinterpret_cast<const int *>(elems), static_cast<int>(count));
|
||||
|
||||
env->ReleasePrimitiveArrayCritical(values, elems, JNI_ABORT);
|
||||
return ok;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_initialize(
|
||||
JNIEnv *env, jobject, jstring rootDir, jstring user, jstring socInfo) {
|
||||
// The core is dlopen()ed separately and may not be up yet -- during
|
||||
@@ -316,6 +374,18 @@ extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_resume(JNIEnv *env,
|
||||
return rpcsxLib.resume();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_pause(JNIEnv *env,
|
||||
jobject) {
|
||||
// Same null guard as resume: the core is dlopen()ed separately and may not be
|
||||
// up yet. A missing symbol also means an older core, so an app built against
|
||||
// this cannot assume the export is there.
|
||||
if (rpcsxLib.pause == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
return rpcsxLib.pause();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL Java_net_rpcsx_RPCSX_openHomeMenu(JNIEnv *env,
|
||||
jobject) {
|
||||
// The core is dlopen()ed separately and may not be up yet -- during
|
||||
@@ -340,6 +410,21 @@ Java_net_rpcsx_RPCSX_getTitleId(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.getTitleId());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_getCurrentTrophyName(JNIEnv *env, jobject) {
|
||||
// The core is dlopen()ed separately and may not be up yet -- during
|
||||
// onboarding, or if it failed to load. Calling through a null pointer
|
||||
// is an instant SIGSEGV, so fail the call instead.
|
||||
//
|
||||
// Also null on an OLDER core that predates this export, since it is resolved
|
||||
// by dlsym: the frontend must treat null as "unknown", not as "no trophies".
|
||||
if (rpcsxLib.getCurrentTrophyName == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return wrap(env, rpcsxLib.getCurrentTrophyName());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL Java_net_rpcsx_RPCSX_surfaceEvent(
|
||||
JNIEnv *env, jobject, jobject surface, jint event) {
|
||||
// The core is dlopen()ed separately and may not be up yet -- during
|
||||
@@ -544,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
|
||||
@@ -556,6 +661,145 @@ Java_net_rpcsx_RPCSX_getVersion(JNIEnv *env, jobject) {
|
||||
return wrap(env, rpcsxLib.getVersion());
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// Why a driver will not load, when the answer is knowable before trying.
|
||||
//
|
||||
// A community driver built against a newer NDK imports symbols versioned against a libc
|
||||
// this device does not have, and the linker then refuses it. adrenotools reports that to
|
||||
// logcat and quietly substitutes the system driver, so from the app's side the load
|
||||
// "succeeded" and the user runs a driver they did not choose.
|
||||
//
|
||||
// The requirement is stated in the file: DT_VERNEED / .gnu.version_r lists the libc
|
||||
// versions it needs, e.g. LIBC_36 for API 36. Comparing that against the running API
|
||||
// turns "failed to load" into the actual reason, which is the difference between a
|
||||
// usable bug report and a shrug. Mr Purple T29 needs LIBC_36 (Android 16) and its own
|
||||
// meta.json claims minApi 30, so the metadata cannot be trusted for this -- only the
|
||||
// binary can.
|
||||
//
|
||||
// Returns an empty string when nothing conclusive was found. Advisory only: the load is
|
||||
// still attempted, so a wrong answer here costs a log line and never a working driver.
|
||||
static std::string driver_libc_requirement_blocker(const std::string &soPath) {
|
||||
std::FILE *f = std::fopen(soPath.c_str(), "rb");
|
||||
if (f == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<char> data;
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
const long size = std::ftell(f);
|
||||
|
||||
// Header tables live near the start and end; the symbol names they point at can be
|
||||
// anywhere, so read the whole file. These are ~15-20 MB and read once per driver
|
||||
// switch, not per boot.
|
||||
if (size <= 0 || size > (256 << 20)) {
|
||||
std::fclose(f);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
data.resize(static_cast<size_t>(size));
|
||||
const size_t got = std::fread(data.data(), 1, data.size(), f);
|
||||
std::fclose(f);
|
||||
|
||||
if (got != data.size() || data.size() < sizeof(Elf64_Ehdr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto *ehdr = reinterpret_cast<const Elf64_Ehdr *>(data.data());
|
||||
|
||||
if (std::memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
ehdr->e_ident[EI_CLASS] != ELFCLASS64 || ehdr->e_shoff == 0 ||
|
||||
ehdr->e_shentsize != sizeof(Elf64_Shdr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Section headers rather than PT_DYNAMIC: they carry file offsets directly, so no
|
||||
// vaddr-to-offset mapping is needed. Shared objects keep them; if they are gone, this
|
||||
// check simply declines to answer.
|
||||
const auto section_at = [&](size_t i) -> const Elf64_Shdr * {
|
||||
const size_t off = ehdr->e_shoff + i * sizeof(Elf64_Shdr);
|
||||
if (off + sizeof(Elf64_Shdr) > data.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<const Elf64_Shdr *>(data.data() + off);
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < ehdr->e_shnum; i++) {
|
||||
const Elf64_Shdr *sh = section_at(i);
|
||||
|
||||
if (sh == nullptr || sh->sh_type != SHT_GNU_verneed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Elf64_Shdr *strtab = section_at(sh->sh_link);
|
||||
|
||||
if (strtab == nullptr || strtab->sh_offset >= data.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char *strings = data.data() + strtab->sh_offset;
|
||||
const size_t strings_max = data.size() - strtab->sh_offset;
|
||||
|
||||
size_t offset = sh->sh_offset;
|
||||
int highest_libc = 0;
|
||||
|
||||
for (size_t entry = 0; entry < sh->sh_info; entry++) {
|
||||
if (offset + sizeof(Elf64_Verneed) > data.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto *vn = reinterpret_cast<const Elf64_Verneed *>(data.data() + offset);
|
||||
size_t aux_offset = offset + vn->vn_aux;
|
||||
|
||||
for (size_t aux = 0; aux < vn->vn_cnt; aux++) {
|
||||
if (aux_offset + sizeof(Elf64_Vernaux) > data.size()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto *vna = reinterpret_cast<const Elf64_Vernaux *>(data.data() + aux_offset);
|
||||
|
||||
if (vna->vna_name < strings_max) {
|
||||
const char *name = strings + vna->vna_name;
|
||||
int level = 0;
|
||||
|
||||
// Only LIBC_<n> is a device-capability statement. Anything else (LIBC,
|
||||
// LIBC_PRIVATE, other sonames) says nothing about the API level.
|
||||
if (std::sscanf(name, "LIBC_%d", &level) == 1 && level > highest_libc) {
|
||||
highest_libc = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (vna->vna_next == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
aux_offset += vna->vna_next;
|
||||
}
|
||||
|
||||
if (vn->vn_next == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += vn->vn_next;
|
||||
}
|
||||
|
||||
const int device_api = android_get_device_api_level();
|
||||
|
||||
if (highest_libc > 0 && device_api > 0 && highest_libc > device_api) {
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"it requires LIBC_%d (Android API %d) but this device provides API %d",
|
||||
highest_libc, highest_libc, device_api);
|
||||
return buf;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
#endif // __aarch64__
|
||||
|
||||
extern "C" JNIEXPORT jboolean JNICALL
|
||||
Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
jstring jlibraryName, jstring jhookDir) {
|
||||
@@ -573,6 +817,26 @@ Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
__android_log_print(ANDROID_LOG_INFO, "RPCSX-UI", "Loading custom driver %s",
|
||||
path.c_str());
|
||||
|
||||
// Said before the attempt, because adrenotools swallows the failure: it logs to
|
||||
// logcat and hands back the system driver, so the caller cannot tell a real load
|
||||
// from a substitution, and the reason never reaches the emulator log at all.
|
||||
if (auto blocker = driver_libc_requirement_blocker(path + "/" + libraryName);
|
||||
!blocker.empty()) {
|
||||
const std::string report =
|
||||
"Custom driver '" + libraryName + "' cannot load on this device: " + blocker +
|
||||
". It was built against a newer NDK than this Android version supports; the "
|
||||
"driver's own metadata does not carry this. The system driver will be used "
|
||||
"instead.";
|
||||
|
||||
__android_log_print(ANDROID_LOG_ERROR, "RPCSX-UI", "%s", report.c_str());
|
||||
|
||||
// Also to the emulator log, which is the file that gets attached to issues.
|
||||
// logcat alone means the reason exists and no report ever contains it.
|
||||
if (rpcsxLib.reportDriverProblem != nullptr) {
|
||||
rpcsxLib.reportDriverProblem(report);
|
||||
}
|
||||
}
|
||||
|
||||
::dlerror();
|
||||
loader = adrenotools_open_libvulkan(
|
||||
RTLD_NOW, ADRENOTOOLS_DRIVER_CUSTOM, nullptr, (hookDir + "/").c_str(),
|
||||
@@ -586,10 +850,27 @@ Java_net_rpcsx_RPCSX_setCustomDriver(JNIEnv *env, jobject, jstring jpath,
|
||||
}
|
||||
}
|
||||
|
||||
auto prevLoader = rpcsxLib.setCustomDriver(loader);
|
||||
if (prevLoader != nullptr) {
|
||||
::dlclose(prevLoader);
|
||||
}
|
||||
// Deliberately NOT dlclose()ing the previous handle.
|
||||
//
|
||||
// A Vulkan driver cannot be unloaded while anything resolved out of it is still reachable, and
|
||||
// from here there is no way to know that. VMA caches vkGetPhysicalDeviceMemoryProperties2 in the
|
||||
// allocator at creation time, so the address lives inside the driver library for as long as the
|
||||
// renderer does.
|
||||
//
|
||||
// Restart is the one flow where a start races a teardown that has not finished:
|
||||
// applyRendererPrefs() re-applies the driver on EVERY start, so it dlopen'd a new handle and
|
||||
// closed the old one while the previous VKGSRender was still unwinding. Its destructor then
|
||||
// freed its data heaps, VMA went to refresh its budget, and called through a pointer into a
|
||||
// library that was no longer mapped -- "Segfault executing location <addr> at <addr>", inside
|
||||
// VmaAllocator_T::UpdateVulkanBudget. The give-away was the fault address landing on the same
|
||||
// offset every time with a different base: a live function in an unmapped library, not a
|
||||
// corrupted pointer. That is the "Restart crashes the app" report, and the same for
|
||||
// apply-and-restart after picking a driver.
|
||||
//
|
||||
// Leaking one handle per driver SWITCH is the cheap side of this trade: it is bounded by how
|
||||
// many times a user changes driver in a session, the mapping is shared, and dlclose on an ICD
|
||||
// is not something the loader promises to honour anyway.
|
||||
rpcsxLib.setCustomDriver(loader);
|
||||
|
||||
return true;
|
||||
#else
|
||||
@@ -663,6 +944,17 @@ Java_net_rpcsx_RPCSX_hasStateInSlot(JNIEnv *, jobject, jint slot) {
|
||||
// Patches
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_net_rpcsx_RPCSX_patchEngineVersion(JNIEnv *env, jobject) {
|
||||
// Empty rather than a guessed version: the caller asks precisely because it
|
||||
// must not name a schema version of its own.
|
||||
if (rpcsxLib.patchEngineVersion == nullptr) {
|
||||
return wrap(env, "");
|
||||
}
|
||||
|
||||
return wrap(env, rpcsxLib.patchEngineVersion());
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_net_rpcsx_RPCSX_patchesImport(JNIEnv *env, jobject, jstring jcontent) {
|
||||
if (rpcsxLib.patchesImport == nullptr) {
|
||||
@@ -702,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 : "");
|
||||
}
|
||||
|
||||
@@ -24,8 +24,17 @@ object DiscIcons {
|
||||
|
||||
fun fileFor(titleId: String): File = File(dir(), "$titleId.png")
|
||||
|
||||
/** True when this game's icon has already been extracted. */
|
||||
fun has(titleId: String): Boolean = fileFor(titleId).isFile
|
||||
/**
|
||||
* True when this game's icon has already been extracted.
|
||||
*
|
||||
* Length, not isFile: an empty file is indistinguishable from a real one to isFile, and
|
||||
* it is a shape this can actually end up in -- the extraction writes to a staging name
|
||||
* and renames, and neither the write nor the rename is checked. An empty icon then reads
|
||||
* as "already extracted" forever, and the card falls through to the text placeholder
|
||||
* because there is nothing for Coil to decode. Requiring bytes makes that self-repairing:
|
||||
* the next scan re-probes and overwrites it.
|
||||
*/
|
||||
fun has(titleId: String): Boolean = fileFor(titleId).length() > 0L
|
||||
|
||||
fun clear() {
|
||||
runCatching { dir().listFiles()?.forEach { it.delete() } }
|
||||
|
||||
@@ -330,7 +330,10 @@ data class GameInfo(
|
||||
* 404, and always match the disc. There is no PS3 equivalent of xlenore's
|
||||
* ps2-covers to point at anyway.
|
||||
*/
|
||||
val discIconFile: java.io.File? get() = serial?.let { DiscIcons.fileFor(it) }?.takeIf { it.isFile }
|
||||
// Length rather than isFile, for the same reason as DiscIcons.has: an empty file passes
|
||||
// isFile, hands Coil something undecodable, and costs the card its placeholder-vs-cover
|
||||
// decision. Nothing is a better answer than zero bytes.
|
||||
val discIconFile: java.io.File? get() = serial?.let { DiscIcons.fileFor(it) }?.takeIf { it.length() > 0L }
|
||||
|
||||
private fun coverUrlFor(s: String): String {
|
||||
// PS3 art comes from aldostools/Resources, which is flat: COV/<TITLE_ID>.JPG
|
||||
|
||||
@@ -22,8 +22,14 @@ object Ps3PatchRepo {
|
||||
* RPCS3's official patch feed. `v` is the patch-engine version the server
|
||||
* uses to decide which schema to hand back, so it is not cosmetic -- an
|
||||
* older value returns patches this core cannot parse.
|
||||
*
|
||||
* The version comes from the core (patch_engine_version) rather than being
|
||||
* written here. It was spelled out as 1.2, which is correct only until
|
||||
* upstream bumps the constant: patch_engine::load rejects any file whose
|
||||
* Version header does not match, so the two have to move together.
|
||||
*/
|
||||
private const val PATCH_URL = "https://rpcs3.net/compatibility?patch&api=v1&v=1.2"
|
||||
private fun patchUrl(version: String) =
|
||||
"https://rpcs3.net/compatibility?patch&api=v1&v=$version"
|
||||
|
||||
data class Patch(
|
||||
val hash: String,
|
||||
@@ -49,11 +55,15 @@ object Ps3PatchRepo {
|
||||
data object Network : Result
|
||||
data class Server(val code: Int) : Result
|
||||
data object Parse : Result
|
||||
data object Checksum : Result
|
||||
}
|
||||
|
||||
fun download(): Result {
|
||||
val engineVersion = runCatching { RPCSX.instance.patchEngineVersion() }.getOrDefault("")
|
||||
if (engineVersion.isBlank()) return Result.Parse
|
||||
|
||||
val res = runCatching {
|
||||
com.armsx3.HttpClient.doRequest(PATCH_URL, userAgent = "ARMSX3")
|
||||
com.armsx3.HttpClient.doRequest(patchUrl(engineVersion), userAgent = "ARMSX3")
|
||||
}.getOrNull() ?: return Result.Network
|
||||
|
||||
if (res.statusCode != 200 || res.data.isEmpty()) return Result.Network
|
||||
@@ -62,19 +72,62 @@ object Ps3PatchRepo {
|
||||
// { "return_code": 0, "version": "1.2", "sha256": "...", "patch": "<yaml>" }
|
||||
// Handing the envelope straight to the YAML parser fails on the first
|
||||
// line, which is exactly what it did.
|
||||
val yaml = runCatching {
|
||||
val envelope = runCatching {
|
||||
val obj = org.json.JSONObject(String(res.data, Charsets.UTF_8))
|
||||
val code = obj.optInt("return_code", -1)
|
||||
if (code != 0) return Result.Server(code)
|
||||
obj.optString("patch")
|
||||
}.getOrNull()
|
||||
obj
|
||||
}.getOrNull() ?: return Result.Parse
|
||||
|
||||
if (yaml.isNullOrBlank()) return Result.Parse
|
||||
// The server picks the schema from the version we asked for, so a reply
|
||||
// for a different one is a server-side surprise rather than something to
|
||||
// hand to the parser: patch_engine::load would reject the whole file on
|
||||
// its Version header anyway, several megabytes later.
|
||||
if (envelope.optString("version") != engineVersion) return Result.Parse
|
||||
|
||||
val yaml = envelope.optString("patch")
|
||||
if (yaml.isBlank()) return Result.Parse
|
||||
|
||||
// Desktop RPCS3 verifies this digest before it writes anything
|
||||
// (patch_manager_dialog::handle_json), and the check was missing here.
|
||||
// Patches are writes into the guest executable, and move_file/hide_file
|
||||
// patches reach the emulator's own filesystem, so content that is not
|
||||
// what the server hashed does not get imported.
|
||||
val expected = envelope.optString("sha256")
|
||||
if (!expected.equals(sha256(yaml), ignoreCase = true)) return Result.Checksum
|
||||
|
||||
val n = runCatching { RPCSX.instance.patchesImport(yaml) }.getOrDefault(-1)
|
||||
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")
|
||||
.digest(text.toByteArray(Charsets.UTF_8))
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
/**
|
||||
* Patches applicable to a serial. An empty serial lists everything, which is
|
||||
* what the standalone tab shows when no game is selected.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user