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 | ||
|
|
93c6df7e42 | ||
|
|
f056d6fc86 | ||
|
|
ae1caf915b | ||
|
|
9128a75c0e | ||
|
|
785c4d5627 | ||
|
|
38424a59bd | ||
|
|
8c707648cb | ||
|
|
9543378661 | ||
|
|
0b5a43c602 | ||
|
|
5997681fed | ||
|
|
49012ba969 | ||
|
|
0909f77a95 | ||
|
|
0cf85cb902 | ||
|
|
cb3670e2db | ||
|
|
5f346b5e2c | ||
|
|
1c2d44502c | ||
|
|
ba5e4ebd66 | ||
|
|
a873ae8250 | ||
|
|
e8c499056b | ||
|
|
88f4162883 | ||
|
|
771a0dc74e | ||
|
|
1458d5f2d9 | ||
|
|
2cd341e257 | ||
|
|
fa97d01b0d | ||
|
|
703d98ae9f | ||
|
|
cf2481fc11 | ||
|
|
8041edf5bc | ||
|
|
d909537f3f | ||
|
|
0cbab0c3ae | ||
|
|
3338eedfcd | ||
|
|
062cde277a | ||
|
|
7b0f1cd6de | ||
|
|
05842b3115 | ||
|
|
d6235c5802 | ||
|
|
94b12dc216 | ||
|
|
ac7639457f | ||
|
|
84db19dc3e | ||
|
|
5eb3d64ed0 | ||
|
|
d82f1df96b | ||
|
|
697cacd854 | ||
|
|
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 = 7
|
||||
versionName = "0.4.1"
|
||||
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.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user