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 |
Vendored
+9
@@ -373,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
|
||||
+52
-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)
|
||||
{
|
||||
|
||||
@@ -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 = 12
|
||||
versionName = "0.7.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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -56,6 +56,9 @@ struct RPCSXApi {
|
||||
std::string (*getUser)();
|
||||
std::string (*settingsGet)(std::string_view path);
|
||||
bool (*settingsSet)(std::string_view path, std::string_view valueString);
|
||||
int (*frameGenImportShaders)(std::string_view path);
|
||||
int (*frameGenShaderCount)();
|
||||
const char *(*frameGenShaderError)();
|
||||
void (*settingsBeginBatch)();
|
||||
void (*settingsEndBatch)();
|
||||
bool (*installSplitPkg)(JNIEnv *env, const int *fds, int count, long progressId);
|
||||
@@ -147,6 +150,12 @@ struct RPCSXLibrary : RPCSXApi {
|
||||
result.getUser = reinterpret_cast<decltype(getUser)>(dlsym(handle, "_rpcsx_getUser"));
|
||||
result.settingsGet = reinterpret_cast<decltype(settingsGet)>(dlsym(handle, "_rpcsx_settingsGet"));
|
||||
result.settingsSet = reinterpret_cast<decltype(settingsSet)>(dlsym(handle, "_rpcsx_settingsSet"));
|
||||
// Resolved without ensure(): a core built before frame generation existed simply has no such
|
||||
// symbol, and refusing to load it over a missing optional feature would be worse than the
|
||||
// feature being absent. The Kotlin side treats a null here as "unsupported".
|
||||
result.frameGenImportShaders = reinterpret_cast<decltype(frameGenImportShaders)>(dlsym(handle, "_rpcsx_frameGenImportShaders"));
|
||||
result.frameGenShaderCount = reinterpret_cast<decltype(frameGenShaderCount)>(dlsym(handle, "_rpcsx_frameGenShaderCount"));
|
||||
result.frameGenShaderError = reinterpret_cast<decltype(frameGenShaderError)>(dlsym(handle, "_rpcsx_frameGenShaderError"));
|
||||
result.settingsBeginBatch = reinterpret_cast<decltype(settingsBeginBatch)>(dlsym(handle, "_rpcsx_settingsBeginBatch"));
|
||||
result.settingsEndBatch = reinterpret_cast<decltype(settingsEndBatch)>(dlsym(handle, "_rpcsx_settingsEndBatch"));
|
||||
result.installSplitPkg = reinterpret_cast<decltype(installSplitPkg)>(dlsym(handle, "_rpcsx_installSplitPkg"));
|
||||
@@ -1011,3 +1020,23 @@ Java_net_rpcsx_RPCSX_getRsxThreadTid(JNIEnv *, jobject) {
|
||||
}
|
||||
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 : "");
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ object ConfigStore {
|
||||
private const val KEY_SPU_DECODER_RESTORE = "config.migrated.spuDecoderRestoreLlvm"
|
||||
private const val KEY_XFLOAT_BACK_TO_APPROX = "config.migrated.xfloatBackToApprox"
|
||||
private const val KEY_PRECISE_SPU_OFF = "config.migrated.preciseSpuVerifyOff"
|
||||
// Oboe became the Android default in 0.7.2; move anyone still on the old Cubeb default.
|
||||
private const val KEY_AUDIO_OBOE = "config.migrated.audioOboeDefault"
|
||||
private const val KEY_ATOMIC_DMA_OFF = "config.migrated.atomicDmaStoresOff"
|
||||
// Bumped: the first pass recorded only Vblank Rate, which did not hold on its own.
|
||||
private const val KEY_VBLANK_60 = "config.migrated.frameCap60"
|
||||
@@ -316,6 +318,16 @@ object ConfigStore {
|
||||
}
|
||||
|
||||
|
||||
// Oboe is the Android default now. Only move people sitting on the previous default
|
||||
// (Cubeb, index 2) -- anyone who deliberately picked Null or another backend keeps it.
|
||||
if (!MainActivityRuntime.prefs.getBoolean(KEY_AUDIO_OBOE, false)) {
|
||||
if (raw != null && parsed.ps3.audioRenderer == 2) {
|
||||
parsed = parsed.copy(ps3 = parsed.ps3.copy(audioRenderer = 4))
|
||||
dirty = true
|
||||
}
|
||||
MainActivityRuntime.prefs.edit { putBoolean(KEY_AUDIO_OBOE, true) }
|
||||
}
|
||||
|
||||
// The ARM64 block checksum is fixed, so the full-compare workaround can go.
|
||||
if (!MainActivityRuntime.prefs.getBoolean(KEY_PRECISE_SPU_OFF, false)) {
|
||||
if (raw != null && parsed.ps3.preciseSpuVerification) {
|
||||
|
||||
@@ -126,6 +126,14 @@ data class Ps3Settings(
|
||||
* wait for their real shader instead of running through the interpreter.
|
||||
*/
|
||||
val shaderMode: Int = 1,
|
||||
/** Lossless Scaling frame generation: 0 Off, 1 x2, 2 x3, 3 x4. Off unless the user has
|
||||
* supplied shaders from their own copy -- nothing is bundled. */
|
||||
val frameGeneration: Int = 0,
|
||||
// Default ON: 3.1p is the cheaper of the two shader families framegen ships, and on a mobile
|
||||
// GPU the full-quality path costs more than the frames it buys.
|
||||
val frameGenPerformance: Boolean = true,
|
||||
// Optical-flow resolution as a percentage of full; lower is cheaper and blurrier in motion.
|
||||
val frameGenFlowScale: Int = 100,
|
||||
val writeColorBuffers: Boolean = false,
|
||||
val writeDepthBuffer: Boolean = false,
|
||||
val readColorBuffers: Boolean = false,
|
||||
@@ -198,6 +206,12 @@ data class Ps3Settings(
|
||||
* preciseSpuVerification). Accurate is a rarely-exercised path and costs
|
||||
* speed, so there is no reason to sit on it.
|
||||
*/
|
||||
/**
|
||||
* Which face button confirms in PS3 system dialogs. 0 = circle, 1 = cross, matching
|
||||
* enter_button_assign. Japanese-region games and hardware confirm with circle; the rest of
|
||||
* the world uses cross, which is why RPCS3 exposes it rather than deriving it from region.
|
||||
*/
|
||||
val enterButtonAssign: Int = 1,
|
||||
val spuXFloat: Int = 1,
|
||||
val accurateSpuRsv: Boolean = true,
|
||||
/**
|
||||
@@ -249,7 +263,8 @@ data class Ps3Settings(
|
||||
val debugConsoleMode: Boolean = false,
|
||||
val resolution: Int = 2,
|
||||
val anisoFilter: Int = 0,
|
||||
val audioRenderer: Int = 2,
|
||||
/** Index into Rpcs3Settings.AUDIO_RENDERERS. 4 = Oboe, the Android default (see node_audio). */
|
||||
val audioRenderer: Int = 4,
|
||||
/**
|
||||
* Output aspect override in permille (1778 = 16:9, 1333 = 4:3), 0 = follow the game.
|
||||
*
|
||||
@@ -271,10 +286,17 @@ data class Ps3Settings(
|
||||
val overlayPosition: Int = 0,
|
||||
// RPCS3 stores these as "#RRGGBBAA" strings. Kept as packed ARGB ints here so
|
||||
// the existing colour picker can drive them, and converted on the way out.
|
||||
val overlayBodyColor: Int = 0xFFE138FF.toInt(),
|
||||
val overlayBodyBg: Int = 0x002339FF,
|
||||
val overlayTitleColor: Int = 0xF26C24FF.toInt(),
|
||||
val overlayTitleBg: Int = 0x00000000,
|
||||
// ARGB, because that is what Android colour ints are and what argbToRgba() converts FROM.
|
||||
//
|
||||
// These used to hold RPCS3's RGBA hex values verbatim (0xFFE138FF and friends), which are the
|
||||
// right colours in the wrong order: argbToRgba then read the leading FF as alpha and rotated
|
||||
// every channel one byte left, turning the default orange #FFE138FF into #E138FFFF. That is
|
||||
// the pink the overlay has always drawn in, and it made the colour pickers look broken --
|
||||
// every value the user chose was rotated the same way, so nothing ever matched.
|
||||
val overlayBodyColor: Int = 0xFFFFE138.toInt(), // core #FFE138FF
|
||||
val overlayBodyBg: Int = 0xFF002339.toInt(), // core #002339FF
|
||||
val overlayTitleColor: Int = 0xFFF26C24.toInt(), // core #F26C24FF
|
||||
val overlayTitleBg: Int = 0x00000000, // core #00000000, fully transparent
|
||||
)
|
||||
|
||||
data class Settings(
|
||||
@@ -1041,6 +1063,9 @@ data class Settings(
|
||||
put("PS3/Overlay", "Title Background (hex)", "string", argbToRgba(ps3.overlayTitleBg))
|
||||
put("PS3/Video", "MSAA", "enum", ps3.msaaMode.toString())
|
||||
put("PS3/Video", "Shader Mode", "enum", ps3.shaderMode.toString())
|
||||
put("PS3/Video", "Frame Generation", "enum", ps3.frameGeneration.toString())
|
||||
put("PS3/Video", "Frame Generation Performance Mode", "bool", ps3.frameGenPerformance.toString())
|
||||
put("PS3/Video", "Frame Generation Flow Scale", "int", ps3.frameGenFlowScale.toString())
|
||||
put("PS3/Video", "Write Color Buffers", "bool", ps3.writeColorBuffers.toString())
|
||||
put("PS3/Video", "Write Depth Buffer", "bool", ps3.writeDepthBuffer.toString())
|
||||
put("PS3/Video", "Read Color Buffers", "bool", ps3.readColorBuffers.toString())
|
||||
@@ -1064,6 +1089,7 @@ data class Settings(
|
||||
put("PS3/Net", "Internet enabled", "enum", ps3.netEnabled.toString())
|
||||
put("PS3/Net", "PSN status", "enum", ps3.psnStatus.toString())
|
||||
put("PS3/Net", "UPNP Enabled", "bool", ps3.upnpEnabled.toString())
|
||||
put("PS3/System", "Enter button assignment", "enum", ps3.enterButtonAssign.toString())
|
||||
put("PS3/Core", "SPU XFloat Accuracy", "enum", ps3.spuXFloat.toString())
|
||||
put("PS3/Core", "Accurate SPU Reservations", "bool", ps3.accurateSpuRsv.toString())
|
||||
put("PS3/Core", "Accurate Cache Line Stores", "bool", ps3.accurateCacheLine.toString())
|
||||
@@ -2008,6 +2034,9 @@ data class Settings(
|
||||
put("ps3MsaaMode", ps3.msaaMode)
|
||||
put("ps3AudioCubebBackend", ps3.audioCubebBackend)
|
||||
put("ps3ShaderMode", ps3.shaderMode)
|
||||
put("ps3FrameGeneration", ps3.frameGeneration)
|
||||
put("ps3FrameGenPerformance", ps3.frameGenPerformance)
|
||||
put("ps3FrameGenFlowScale", ps3.frameGenFlowScale)
|
||||
put("ps3WriteColorBuffers", ps3.writeColorBuffers)
|
||||
put("ps3GpuTurbo", ps3.gpuTurbo)
|
||||
put("ps3SilenceAllLogs", ps3.silenceAllLogs)
|
||||
@@ -2032,6 +2061,7 @@ data class Settings(
|
||||
put("ps3NetEnabled", ps3.netEnabled)
|
||||
put("ps3PsnStatus", ps3.psnStatus)
|
||||
put("ps3UpnpEnabled", ps3.upnpEnabled)
|
||||
put("ps3EnterButtonAssign", ps3.enterButtonAssign)
|
||||
put("ps3SpuXFloat", ps3.spuXFloat)
|
||||
put("ps3AccurateSpuRsv", ps3.accurateSpuRsv)
|
||||
put("ps3AccurateCacheLine", ps3.accurateCacheLine)
|
||||
@@ -2346,6 +2376,9 @@ data class Settings(
|
||||
msaaMode = json.optInt("ps3MsaaMode", def.ps3.msaaMode),
|
||||
audioCubebBackend = json.optInt("ps3AudioCubebBackend", def.ps3.audioCubebBackend),
|
||||
shaderMode = json.optInt("ps3ShaderMode", def.ps3.shaderMode),
|
||||
frameGeneration = json.optInt("ps3FrameGeneration", def.ps3.frameGeneration),
|
||||
frameGenPerformance = json.optBoolean("ps3FrameGenPerformance", def.ps3.frameGenPerformance),
|
||||
frameGenFlowScale = json.optInt("ps3FrameGenFlowScale", def.ps3.frameGenFlowScale),
|
||||
writeColorBuffers = json.optBoolean("ps3WriteColorBuffers", def.ps3.writeColorBuffers),
|
||||
gpuTurbo = json.optBoolean("ps3GpuTurbo", def.ps3.gpuTurbo),
|
||||
silenceAllLogs = json.optBoolean("ps3SilenceAllLogs", def.ps3.silenceAllLogs),
|
||||
@@ -2370,6 +2403,7 @@ data class Settings(
|
||||
netEnabled = json.optBoolean("ps3NetEnabled", def.ps3.netEnabled),
|
||||
psnStatus = json.optBoolean("ps3PsnStatus", def.ps3.psnStatus),
|
||||
upnpEnabled = json.optBoolean("ps3UpnpEnabled", def.ps3.upnpEnabled),
|
||||
enterButtonAssign = json.optInt("ps3EnterButtonAssign", def.ps3.enterButtonAssign),
|
||||
spuXFloat = json.optInt("ps3SpuXFloat", def.ps3.spuXFloat),
|
||||
accurateSpuRsv = json.optBoolean("ps3AccurateSpuRsv", def.ps3.accurateSpuRsv),
|
||||
accurateCacheLine = json.optBoolean("ps3AccurateCacheLine", def.ps3.accurateCacheLine),
|
||||
@@ -2664,6 +2698,9 @@ data class Settings(
|
||||
if (current.ps3.msaaMode != base.ps3.msaaMode) j.put("ps3MsaaMode", current.ps3.msaaMode)
|
||||
if (current.ps3.audioCubebBackend != base.ps3.audioCubebBackend) j.put("ps3AudioCubebBackend", current.ps3.audioCubebBackend)
|
||||
if (current.ps3.shaderMode != base.ps3.shaderMode) j.put("ps3ShaderMode", current.ps3.shaderMode)
|
||||
if (current.ps3.frameGeneration != base.ps3.frameGeneration) j.put("ps3FrameGeneration", current.ps3.frameGeneration)
|
||||
if (current.ps3.frameGenPerformance != base.ps3.frameGenPerformance) j.put("ps3FrameGenPerformance", current.ps3.frameGenPerformance)
|
||||
if (current.ps3.frameGenFlowScale != base.ps3.frameGenFlowScale) j.put("ps3FrameGenFlowScale", current.ps3.frameGenFlowScale)
|
||||
if (current.ps3.writeColorBuffers != base.ps3.writeColorBuffers) j.put("ps3WriteColorBuffers", current.ps3.writeColorBuffers)
|
||||
if (current.ps3.gpuTurbo != base.ps3.gpuTurbo) j.put("ps3GpuTurbo", current.ps3.gpuTurbo)
|
||||
if (current.ps3.silenceAllLogs != base.ps3.silenceAllLogs) j.put("ps3SilenceAllLogs", current.ps3.silenceAllLogs)
|
||||
@@ -2688,6 +2725,7 @@ data class Settings(
|
||||
if (current.ps3.netEnabled != base.ps3.netEnabled) j.put("ps3NetEnabled", current.ps3.netEnabled)
|
||||
if (current.ps3.psnStatus != base.ps3.psnStatus) j.put("ps3PsnStatus", current.ps3.psnStatus)
|
||||
if (current.ps3.upnpEnabled != base.ps3.upnpEnabled) j.put("ps3UpnpEnabled", current.ps3.upnpEnabled)
|
||||
if (current.ps3.enterButtonAssign != base.ps3.enterButtonAssign) j.put("ps3EnterButtonAssign", current.ps3.enterButtonAssign)
|
||||
if (current.ps3.spuXFloat != base.ps3.spuXFloat) j.put("ps3SpuXFloat", current.ps3.spuXFloat)
|
||||
if (current.ps3.accurateSpuRsv != base.ps3.accurateSpuRsv) j.put("ps3AccurateSpuRsv", current.ps3.accurateSpuRsv)
|
||||
if (current.ps3.accurateCacheLine != base.ps3.accurateCacheLine) j.put("ps3AccurateCacheLine", current.ps3.accurateCacheLine)
|
||||
@@ -2963,6 +3001,9 @@ data class Settings(
|
||||
msaaMode = if (overrides.has("ps3MsaaMode")) overrides.getInt("ps3MsaaMode") else base.ps3.msaaMode,
|
||||
audioCubebBackend = if (overrides.has("ps3AudioCubebBackend")) overrides.getInt("ps3AudioCubebBackend") else base.ps3.audioCubebBackend,
|
||||
shaderMode = if (overrides.has("ps3ShaderMode")) overrides.getInt("ps3ShaderMode") else base.ps3.shaderMode,
|
||||
frameGeneration = if (overrides.has("ps3FrameGeneration")) overrides.getInt("ps3FrameGeneration") else base.ps3.frameGeneration,
|
||||
frameGenPerformance = if (overrides.has("ps3FrameGenPerformance")) overrides.getBoolean("ps3FrameGenPerformance") else base.ps3.frameGenPerformance,
|
||||
frameGenFlowScale = if (overrides.has("ps3FrameGenFlowScale")) overrides.getInt("ps3FrameGenFlowScale") else base.ps3.frameGenFlowScale,
|
||||
writeColorBuffers = if (overrides.has("ps3WriteColorBuffers")) overrides.getBoolean("ps3WriteColorBuffers") else base.ps3.writeColorBuffers,
|
||||
gpuTurbo = if (overrides.has("ps3GpuTurbo")) overrides.getBoolean("ps3GpuTurbo") else base.ps3.gpuTurbo,
|
||||
silenceAllLogs = if (overrides.has("ps3SilenceAllLogs")) overrides.getBoolean("ps3SilenceAllLogs") else base.ps3.silenceAllLogs,
|
||||
@@ -2987,6 +3028,7 @@ data class Settings(
|
||||
netEnabled = if (overrides.has("ps3NetEnabled")) overrides.getBoolean("ps3NetEnabled") else base.ps3.netEnabled,
|
||||
psnStatus = if (overrides.has("ps3PsnStatus")) overrides.getBoolean("ps3PsnStatus") else base.ps3.psnStatus,
|
||||
upnpEnabled = if (overrides.has("ps3UpnpEnabled")) overrides.getBoolean("ps3UpnpEnabled") else base.ps3.upnpEnabled,
|
||||
enterButtonAssign = if (overrides.has("ps3EnterButtonAssign")) overrides.getInt("ps3EnterButtonAssign") else base.ps3.enterButtonAssign,
|
||||
spuXFloat = if (overrides.has("ps3SpuXFloat")) overrides.getInt("ps3SpuXFloat") else base.ps3.spuXFloat,
|
||||
accurateSpuRsv = if (overrides.has("ps3AccurateSpuRsv")) overrides.getBoolean("ps3AccurateSpuRsv") else base.ps3.accurateSpuRsv,
|
||||
accurateCacheLine = if (overrides.has("ps3AccurateCacheLine")) overrides.getBoolean("ps3AccurateCacheLine") else base.ps3.accurateCacheLine,
|
||||
|
||||
@@ -525,6 +525,14 @@ val EN: Map<String, String> = mapOf(
|
||||
"adv.accurateRsxRsv.description" to "Synchronises GPU access to reserved memory strictly. Fixes rare graphical corruption at a performance cost.",
|
||||
"adv.ppuRsvPriority.label" to "PPU Reservation Priority",
|
||||
"adv.ppuRsvPriority.description" to "Gives the main CPU priority over the SPUs when competing for the same memory. Can help games that stall waiting on the PPU.",
|
||||
"pad.section.enterButton" to "Enter Button Assignment",
|
||||
"pad.enterButton.label" to "Confirm button",
|
||||
"pad.enterButton.circle" to "Enter with circle",
|
||||
"pad.enterButton.cross" to "Enter with cross",
|
||||
"pad.enterButton.description" to "Which button confirms in PS3 system dialogs. Japanese games usually expect circle; most others use cross.",
|
||||
"app.resetAll" to "Reset all settings",
|
||||
"app.resetAll.desc" to "Put every setting back to its default. Per-game settings and controller binds are kept.",
|
||||
"app.resetAll.confirm" to "Every global setting goes back to its default. Per-game overrides and controller binds are not touched.",
|
||||
"adv.spuVerification.label" to "SPU Verification",
|
||||
"adv.spuVerification.description" to "Verifies compiled SPU code against the original. Catches miscompiles; turning it off is faster but makes bad codegen silent.",
|
||||
"adv.preciseSpuVerification.label" to "Precise SPU Verification",
|
||||
@@ -703,6 +711,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"overlay.uiSize.description" to "Scales menu/library padding and control sizes. 100% = default.",
|
||||
"overlay.osdColor.label" to "OSD Color",
|
||||
"overlay.osdColor.description" to "Color of the on-screen display text (FPS, stats and notifications). Speed warnings stay red/green so they still stand out.",
|
||||
"overlay.osdColor.custom" to "Custom",
|
||||
"overlay.osdColor.default" to "White",
|
||||
"overlay.osdColor.green" to "Green",
|
||||
"overlay.osdColor.cyan" to "Cyan",
|
||||
@@ -821,6 +830,8 @@ val EN: Map<String, String> = mapOf(
|
||||
"pad.stickFeel.acceleration.description" to "Non-linear response curve: small tilts stay precise for aiming, full tilt ramps up to full speed. 0 = linear (off); higher = more curve.",
|
||||
"pad.stickFeel.acceleration.label" to "Acceleration",
|
||||
"pad.stickFeel.antiDeadzone.description" to "Smallest output sent to the game, to cancel a game's OWN built-in stick deadzone (e.g. Cold Fear / Area 51 ignore the stick until ~45%, then aim jumps). Set near the game's deadzone so any stick movement responds immediately and the full travel maps smoothly above it. 0 = off.",
|
||||
"pad.stickFeel.squareGate.label" to "Full Diagonal Range",
|
||||
"pad.stickFeel.squareGate.description" to "Sends the full range on diagonals instead of the reduced value a real DualShock gives (~70%), so diagonal movement is as fast as straight up/down/left/right. On by default. Turn off to match original hardware exactly.",
|
||||
"pad.stickFeel.antiDeadzone.label" to "Anti-Deadzone",
|
||||
"pad.stickFeel.deadzone.description" to "Fraction of physical analog travel ignored near center (applied to the stick's radial distance, so diagonals behave like cardinals). Output re-normalizes past it, so movement still ramps smoothly from 0 — which also means the on-screen effect can be masked by a game's OWN built-in deadzone (Area 51 ignores input below ~45% no matter what you set here; use Anti-Deadzone for that). 0 = off — raw hardware values pass through, including any stick drift.",
|
||||
"pad.stickFeel.deadzone.label" to "Deadzone",
|
||||
@@ -1039,6 +1050,22 @@ val EN: Map<String, String> = mapOf(
|
||||
"perf.decoder.interpreterDyn" to "Interpreter (dyn)",
|
||||
"perf.decoder.asmjit" to "ASMJIT",
|
||||
"perf.decoder.llvm" to "LLVM",
|
||||
"perf.framegen.title" to "Frame Generation (Experimental)",
|
||||
"perf.framegen.import" to "Import from Lossless Scaling\u2026",
|
||||
"perf.framegen.import.missing" to "Shaders not imported \u2014 frame generation will not run.",
|
||||
"perf.framegen.import.ok" to "Shaders imported (%d).",
|
||||
"perf.framegen.import.working" to "Reading shaders\u2026",
|
||||
"perf.framegen.import.failed" to "Could not read shaders from that file.",
|
||||
"perf.framegen.label" to "Lossless Scaling",
|
||||
"perf.framegen.performance.label" to "Performance shaders",
|
||||
"perf.framegen.performance.description" to "Use Lossless Scaling's lighter 3.1p shaders instead of the full-quality 3.1 set. Cheaper to run and slightly softer in motion \u2014 on by default, because the quality set usually costs more than the frames it buys on a phone. Both come from the file you imported, so switching does not need another import.\n\nTakes effect when frame generation next starts: turn it off and on again, or restart the game.",
|
||||
"perf.framegen.flowScale.label" to "Motion detail",
|
||||
"perf.framegen.flowScale.description" to "How finely motion is measured between frames, as a percentage of full resolution. Lower is faster and blurrier around moving edges. Drop this before dropping the multiplier if frame generation is costing more than it gives.\n\nTakes effect when frame generation next starts.",
|
||||
"perf.framegen.off" to "Off",
|
||||
"perf.framegen.x2" to "x2",
|
||||
"perf.framegen.x3" to "x3",
|
||||
"perf.framegen.x4" to "x4",
|
||||
"perf.framegen.description" to "EXPERIMENTAL. Insert generated frames between the ones the game actually draws. Costs GPU time and adds latency, so it helps when the CPU is the limit and hurts when the GPU already is.\n\nIt works best from a steady framerate. Interpolating a game that is already struggling tends to look worse rather than better \u2014 generated frames land at the wrong moment when the real interval keeps changing, which reads as judder. A locked 25 usually looks better than a wandering 28.\n\nOn-screen text shimmers or flickers while this is on \u2014 the overlay and the game\u0027s own menus get interpolated along with everything else, and fine text is what that looks worst on. That is how frame generation behaves, not a fault. Turning it off restores steady text. Switching it on or off during a game also pauses for a few seconds while the shaders are prepared.\n\nThis does nothing until you import Lossless.dll below. It is part of Lossless Scaling on Steam \u2014 you need your own copy, and nothing is bundled or downloaded. On Windows the file sits in steamapps\\common\\Lossless Scaling\\Lossless.dll; copy it to your device and pick it with the button below. Only the shaders are kept, and your copy of the file is deleted afterwards.",
|
||||
"perf.ppuDecoder.label" to "PPU Decoder",
|
||||
"perf.ppuDecoder.description" to "How the PS3's main CPU (PPU) is executed. LLVM recompiles PowerPC to native ARM64 and is enormously faster \u2014 keep it unless you are debugging. Interpreter is only for diagnosing a game LLVM gets wrong.",
|
||||
"perf.spuDecoder.label" to "SPU Decoder",
|
||||
|
||||
@@ -364,6 +364,22 @@ object ControllerMappings {
|
||||
private const val KEY_STICK_ANTIDZ = "pad.stick.antiDeadzone"
|
||||
const val STICK_ANTIDZ_MAX = 0.60f
|
||||
private val prefStickAntiDz = PerStickPref(KEY_STICK_ANTIDZ, 0.0f, 0f, STICK_ANTIDZ_MAX)
|
||||
|
||||
// Square gate: send the full per-axis range on diagonals instead of capping them to the
|
||||
// unit circle. A DualShock 3 is circular-gated, so a full diagonal is ~0.707 per axis, and
|
||||
// emitting that is technically faithful -- but it lands inside the internal deadzone of games
|
||||
// that test each axis separately, and their camera then crawls diagonally while the cardinals
|
||||
// are fine. Oblivion is the case that found this.
|
||||
//
|
||||
// DEFAULT ON. Faithfulness to a circular gate is not worth a control scheme that feels broken,
|
||||
// and a modern pad's own gate is closer to square anyway. Off restores the hardware curve for
|
||||
// anyone who wants it. Per stick.
|
||||
private const val KEY_STICK_SQUARE = "pad.stick.square"
|
||||
fun stickSquareGate(left: Boolean): Boolean =
|
||||
MainActivityRuntime.prefs.getBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", true)
|
||||
fun setStickSquareGate(left: Boolean, v: Boolean) =
|
||||
MainActivityRuntime.prefs.edit { putBoolean(KEY_STICK_SQUARE + if (left) ".l" else ".r", v) }
|
||||
|
||||
fun stickAntiDeadzone(left: Boolean): Float = prefStickAntiDz.get(left)
|
||||
fun setStickAntiDeadzone(left: Boolean, v: Float) = prefStickAntiDz.set(left, v)
|
||||
|
||||
|
||||
@@ -57,6 +57,41 @@ class EmulationSurface(context: Context) :
|
||||
super.onAttachedToWindow()
|
||||
hostWindow()?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
displayManager.registerDisplayListener(this, null)
|
||||
redeliverSurface()
|
||||
}
|
||||
|
||||
override fun onWindowVisibilityChanged(visibility: Int) {
|
||||
super.onWindowVisibilityChanged(visibility)
|
||||
if (visibility == VISIBLE) redeliverSurface()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand the current Surface to native again, if we already have a usable one.
|
||||
*
|
||||
* surfaceChanged is a ONE-SHOT: Android delivers it when the surface is created or resized and
|
||||
* never repeats it. The native renderer blocks in getNativeWindow() until that single delivery
|
||||
* arrives — a 100 ms sleep loop with no timeout — so if it is ever missed, the RSX thread parks
|
||||
* forever and the game area stays black at 0% CPU with the boot log stopping dead just after
|
||||
* Vulkan device creation. That is the "launch a game the instant the app opens and get a black
|
||||
* screen" report: the SurfaceView and its compositor layer exist, the touch controls draw over
|
||||
* it, and nothing is wrong except that native was never told. Rotating the device "fixed" it
|
||||
* only because a configuration change forces a fresh surfaceChanged.
|
||||
*
|
||||
* Re-delivering is safe and idempotent: the native side compares the incoming ANativeWindow
|
||||
* against the one it holds and treats an identical pointer as a no-op, so calling this on every
|
||||
* attach and every window-visibility change costs nothing when the surface already arrived.
|
||||
*/
|
||||
fun redeliverSurface() {
|
||||
// post() rather than inline: onAttachedToWindow runs before layout, so width/height are
|
||||
// still 0 here, and a 0x0 report is exactly what the native side is told to ignore.
|
||||
post {
|
||||
val current = holder.surface
|
||||
|
||||
if (current != null && current.isValid && width > 0 && height > 0) {
|
||||
pushDisplayCutoutInset(width, height)
|
||||
NativeApp.onNativeSurfaceChanged(current, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
|
||||
@@ -4361,7 +4361,12 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
val mag = kotlin.math.hypot(gx, gy)
|
||||
if (mag <= 0f) return
|
||||
val shaped = shapeStickMag(mag.coerceAtMost(1f), left)
|
||||
val scale = shaped / mag // preserves direction; caps square-gate diagonals at unit circle
|
||||
// Dividing by the magnitude caps a full diagonal at the unit circle: 0.707 per axis, which
|
||||
// is what a circular-gated DualShock 3 really sends. Games that deadzone each axis on its
|
||||
// own then ignore diagonals almost entirely. Dividing by the LARGER axis instead expands
|
||||
// to the square, so a full diagonal reaches 1.0 on both. Same result on the cardinals.
|
||||
val denom = if (ControllerMappings.stickSquareGate(left)) kotlin.math.max(abs(gx), abs(gy)) else mag
|
||||
val scale = if (denom > 0f) shaped / denom else 0f
|
||||
val ox = gx * scale
|
||||
val oy = gy * scale
|
||||
if (ox > 0f) accumAnalog(aXPos, ox) else if (ox < 0f) accumAnalog(aXNeg, -ox)
|
||||
|
||||
@@ -710,15 +710,41 @@ private fun SessionPane(state: EmulationMenuUiState, viewModel: EmulationMenuVie
|
||||
// than carrying its own copy. Safe to add here: this card's rows are plain switches with
|
||||
// their own callbacks — SessionPane's selectedAction indexes the action GRID above, not
|
||||
// these, so inserting a row can't shift the controller dispatch.
|
||||
val osdColorIndex = com.armsx2.ui.settings.OSD_COLORS
|
||||
.indexOf(state.settings.osdColor).coerceAtLeast(0)
|
||||
//
|
||||
// Writes RPCS3's overlay body colour. It used to write `osdColor`, i.e. PCSX2's
|
||||
// EmuCore/GS/OsdColor plus a stubbed NativeApp.osdSetColor(), so cycling this row in a
|
||||
// game changed nothing whatsoever — the most visible place for a control that did not work.
|
||||
val osdColorIndex = com.armsx2.ui.settings.osdPresetIndex(state.settings.ps3.overlayBodyColor)
|
||||
MenuCycleRow(
|
||||
title = str("overlay.osdColor.label"),
|
||||
valueLabel = str(com.armsx2.ui.settings.OSD_COLOR_LABEL_KEYS[osdColorIndex]),
|
||||
// A colour set with the RGBA sliders is on no preset; say so rather than naming
|
||||
// whichever preset happens to sit at index 0.
|
||||
valueLabel = if (osdColorIndex >= 0)
|
||||
str(com.armsx2.ui.settings.OSD_COLOR_LABEL_KEYS[osdColorIndex])
|
||||
else str("overlay.osdColor.custom"),
|
||||
) { step ->
|
||||
val size = com.armsx2.ui.settings.OSD_COLORS.size
|
||||
val next = ((osdColorIndex + step) % size + size) % size
|
||||
viewModel.updateSettings { it.copy(osdColor = com.armsx2.ui.settings.OSD_COLORS[next]) }
|
||||
viewModel.updateSettings {
|
||||
it.copy(ps3 = it.ps3.copy(overlayBodyColor = com.armsx2.ui.settings.OSD_COLORS[next]))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
// Where the overlay sits. Only the All Settings tab had this, which made it unreachable
|
||||
// at the one moment it matters -- when the stats are sitting on top of something in the
|
||||
// game you are trying to look at.
|
||||
val osdPositionLabels = listOf(
|
||||
"overlay.position.topLeft", "overlay.position.topRight",
|
||||
"overlay.position.bottomLeft", "overlay.position.bottomRight",
|
||||
)
|
||||
val osdPositionIndex = state.settings.ps3.overlayPosition.coerceIn(0, 3)
|
||||
MenuCycleRow(
|
||||
title = str("overlay.position.label"),
|
||||
valueLabel = str(osdPositionLabels[osdPositionIndex]),
|
||||
) { step ->
|
||||
val size = osdPositionLabels.size
|
||||
val next = ((osdPositionIndex + step) % size + size) % size
|
||||
viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(overlayPosition = next)) }
|
||||
}
|
||||
}
|
||||
SectionCard(str("savestate.title.loadManage")) {
|
||||
@@ -1012,6 +1038,20 @@ private fun PerformancePane(state: EmulationMenuUiState, viewModel: EmulationMen
|
||||
// Removed: PS2 wait-loop detection.
|
||||
// The PS3's processors, not the PS2's. EE/IOP/VU0/VU1/Fastmem are PCSX2
|
||||
// recompiler toggles for silicon that does not exist here.
|
||||
// Frame generation first: it is the one setting here that changes the framerate rather than
|
||||
// how fast the emulator runs, so it is what someone opening this menu mid-game is looking for.
|
||||
SectionCard(str("perf.framegen.title")) {
|
||||
HorizontalOptions(
|
||||
title = str("perf.framegen.label"),
|
||||
options = listOf(
|
||||
str("perf.framegen.off"), str("perf.framegen.x2"),
|
||||
str("perf.framegen.x3"), str("perf.framegen.x4"),
|
||||
).mapIndexed { index, label -> index to label },
|
||||
selected = settings.ps3.frameGeneration,
|
||||
onSelect = { v -> viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(frameGeneration = v)) } },
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
SectionCard(str("perf.ps3cpu.title")) {
|
||||
HorizontalOptions(
|
||||
title = str("perf.ppuDecoder.label"),
|
||||
|
||||
@@ -792,6 +792,7 @@ fun AppTab() {
|
||||
)
|
||||
|
||||
ClearCacheRow()
|
||||
ResetAllSettingsRow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,6 +837,77 @@ private fun ClearCacheRow() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Put every global setting back to its default in one go.
|
||||
*
|
||||
* The per-tab Reset in the top bar only covers the page you are looking at, which is right for
|
||||
* undoing one experiment but tedious when a config has drifted across half a dozen tabs. This is
|
||||
* the "start clean" button. Per-game overrides are deliberately left alone: they belong to
|
||||
* individual games, are invisible from here, and wiping them from a global page would be a
|
||||
* surprise. Controller binds live in ControllerMappings and keep their own reset. */
|
||||
@Composable
|
||||
private fun ResetAllSettingsRow() {
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
|
||||
Surface(
|
||||
onClick = { confirming = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.controllerFocusable("app.resetAll", RoundedCornerShape(20.dp), onConfirm = { confirming = true }),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.72f),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.46f)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(46.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
color = MaterialTheme.colorScheme.errorContainer,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||
Text("↺", fontSize = 21.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(str("app.resetAll"), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
str("app.resetAll.desc"),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (confirming) {
|
||||
com.armsx2.ui.common.ConfirmOverlay(
|
||||
title = str("app.resetAll"),
|
||||
message = str("app.resetAll.confirm"),
|
||||
confirmLabel = str("action.reset"),
|
||||
destructive = true,
|
||||
idPrefix = "settings-reset-all",
|
||||
onConfirm = {
|
||||
val defaults = com.armsx2.config.Settings()
|
||||
com.armsx2.ui.InGameOverlay.settingsState.value = defaults
|
||||
com.armsx2.config.ConfigStore.saveGlobal(defaults)
|
||||
|
||||
// Push straight to the core when a game is live, the same way the per-tab reset
|
||||
// does. Without this the UI shows defaults while the running VM keeps the old
|
||||
// values until the next boot.
|
||||
if (MainActivityRuntime.nativeReady.value &&
|
||||
MainActivityRuntime.eState.value != com.armsx2.EmuState.STOPPED) {
|
||||
runCatching { defaults.applyTo() }
|
||||
}
|
||||
|
||||
confirming = false
|
||||
},
|
||||
onDismiss = { confirming = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Export / import everything a reinstall would destroy: save states, memory cards, artwork,
|
||||
* per-game settings, controller profiles, patches and every preference. ROMs and BIOS are left
|
||||
* out — those live outside the app and survive on their own. See [com.armsx2.BackupManager]. */
|
||||
|
||||
@@ -35,16 +35,19 @@ import androidx.core.content.edit
|
||||
* Internal, not private: the in-game quick menu cycles the same palette, and two copies would
|
||||
* drift the moment one gains a colour. */
|
||||
internal val OSD_COLORS = listOf(
|
||||
0x000000, // default (white — 0 means "unset" to the renderer)
|
||||
0x66FF66, // green
|
||||
0x66E0FF, // cyan
|
||||
0xFFE066, // yellow
|
||||
0xFFA64D, // orange
|
||||
0xFF6666, // red
|
||||
0xFF7AC8, // pink
|
||||
0xC08CFF, // purple
|
||||
0xFFFFFFFF.toInt(), // white
|
||||
0xFF66FF66.toInt(), // green
|
||||
0xFF66E0FF.toInt(), // cyan
|
||||
0xFFFFE066.toInt(), // yellow
|
||||
0xFFFFA64D.toInt(), // orange
|
||||
0xFFFF6666.toInt(), // red
|
||||
0xFFFF7AC8.toInt(), // pink
|
||||
0xFFC08CFF.toInt(), // purple
|
||||
)
|
||||
|
||||
/** Which preset [argb] is, or -1 for a colour picked with the RGBA sliders instead. */
|
||||
internal fun osdPresetIndex(argb: Int): Int = OSD_COLORS.indexOf(argb)
|
||||
|
||||
/** i18n keys for [OSD_COLORS], same order. */
|
||||
internal val OSD_COLOR_LABEL_KEYS = listOf(
|
||||
"overlay.osdColor.default", "overlay.osdColor.green", "overlay.osdColor.cyan",
|
||||
@@ -99,14 +102,21 @@ fun OverlayTab(state: MutableState<Settings>) {
|
||||
|
||||
// OSD text colour. A preset row rather than an RGB picker: SegmentedRow is already
|
||||
// controller-navigable (Left/Right/Confirm), whereas a colour wheel would demand
|
||||
// pointer input and strand pad-only devices. 0 = leave it white, so nobody's OSD
|
||||
// changes appearance until they choose to.
|
||||
// pointer input and strand pad-only devices. The RGBA sliders further down set the
|
||||
// same value for anyone who wants a colour that is not on this list.
|
||||
//
|
||||
// This used to write `osdColor`, which is PCSX2's EmuCore/GS/OsdColor plus a
|
||||
// NativeApp.osdSetColor() that is an Unsupported.note() stub in this app -- so it did
|
||||
// nothing at all here, on either the settings tab or the in-game menu, and the OSD
|
||||
// stayed whatever RPCS3's default was. It writes RPCS3's own overlay body colour now.
|
||||
SegmentedRow(
|
||||
label = str("overlay.osdColor.label"),
|
||||
options = OSD_COLOR_LABEL_KEYS.map { str(it) },
|
||||
selectedIndex = OSD_COLORS.indexOf(s.osdColor).coerceAtLeast(0),
|
||||
// No match means the RGBA sliders were used; -1 leaves every segment unselected
|
||||
// rather than lying about which preset is active.
|
||||
selectedIndex = osdPresetIndex(s.ps3.overlayBodyColor),
|
||||
description = str("overlay.osdColor.description"),
|
||||
onChange = { apply(s.copy(osdColor = OSD_COLORS[it])) },
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(overlayBodyColor = OSD_COLORS[it]))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import kotlinx.coroutines.withContext
|
||||
import com.armsx3.NativeApp
|
||||
|
||||
@Composable
|
||||
fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
|
||||
fun PadTab(state: MutableState<Settings>) {
|
||||
val scroll = settingsScrollState()
|
||||
ControllerAutoScroll(scroll)
|
||||
val capture = remember { mutableStateOf<ControllerMappings.Action?>(null) }
|
||||
@@ -448,6 +448,24 @@ fun PadTab(@Suppress("UNUSED_PARAMETER") state: MutableState<Settings>) {
|
||||
// (com.armsx2.ui.settings.GyroSection). Here it follows the Pad tab's Global/Game
|
||||
// scope (editSerial) and shares the tab's refreshToken so it re-reads live.
|
||||
GyroSection(editSerial = editSerial, externalRefresh = refreshToken)
|
||||
// Which face button the PS3 itself treats as "confirm" in system dialogs. This is a
|
||||
// console setting (cellSysutil ID_ENTER_BUTTON_ASSIGN), not a pad remap: it changes what
|
||||
// the GAME asks for, so it has to live in the config rather than in the bind table.
|
||||
// Japanese titles generally expect circle and can read as inverted without it.
|
||||
CollapsibleSection(str("pad.section.enterButton"), initiallyExpanded = false) {
|
||||
SegmentedRow(
|
||||
label = str("pad.enterButton.label"),
|
||||
options = listOf(str("pad.enterButton.circle"), str("pad.enterButton.cross")),
|
||||
selectedIndex = state.value.ps3.enterButtonAssign.coerceIn(0, 1),
|
||||
description = str("pad.enterButton.description"),
|
||||
onChange = { idx ->
|
||||
com.armsx2.ui.InGameOverlay.saveSettings(
|
||||
state.value.copy(ps3 = state.value.ps3.copy(enterButtonAssign = idx)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
CollapsibleSection(str("pad.section.buttonMapping"), initiallyExpanded = false) {
|
||||
ControllerMappings.actions.forEach { action ->
|
||||
val physical = ControllerMappings.physicalForScope(action, editPlayer.intValue, editSerial)
|
||||
@@ -899,6 +917,12 @@ private fun StickFeelSliders(left: Boolean, title: String, refreshToken: Mutable
|
||||
valueFormatter = { "${it * 5}%" },
|
||||
onChange = { ControllerMappings.setStickSensitivity(left, it / 20f); refreshToken.value++ },
|
||||
)
|
||||
|
||||
ToggleRow(
|
||||
str("pad.stickFeel.squareGate.label"),
|
||||
ControllerMappings.stickSquareGate(left),
|
||||
description = str("pad.stickFeel.squareGate.description"),
|
||||
) { ControllerMappings.setStickSquareGate(left, it); refreshToken.value++ }
|
||||
SettingsDivider()
|
||||
IntSliderRow(
|
||||
label = str("pad.stickFeel.acceleration.label"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user