diff --git a/PERFORMANCE_OPTIMIZATION_REPORT.md b/PERFORMANCE_OPTIMIZATION_REPORT.md
deleted file mode 100644
index f7faf15..0000000
--- a/PERFORMANCE_OPTIMIZATION_REPORT.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# PCSX2 ARM64 Performance Optimization Report
-
-## Executive Summary
-
-This report identifies 5 key areas for performance optimization in the PCSX2 ARM64 emulator codebase. The analysis focused on memory allocation patterns, redundant operations, and ARM64-specific optimization opportunities.
-
-## 1. Instruction Cache Reallocation Issue (HIGH PRIORITY)
-
-**Location**: `app/src/main/cpp/pcsx2/x86/ix86-32/iR5900.cpp` lines 2708-2714
-
-**Issue**: The instruction cache (`s_pInstCache`) is frequently reallocated during block recompilation using a naive growth strategy. Every time a block requires more instructions than the current cache size, the entire cache is freed and reallocated with only a small increment (+10 instructions).
-
-**Code Pattern**:
-```cpp
-if (s_nInstCacheSize < (s_nEndBlock - startpc) / 4 + 1)
-{
- free(s_pInstCache);
- s_nInstCacheSize = (s_nEndBlock - startpc) / 4 + 10;
- s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize);
-}
-```
-
-**Impact**: HIGH - This occurs during every block recompilation that exceeds the current cache size, causing:
-- Frequent malloc/free operations (expensive system calls)
-- Memory fragmentation
-- Loss of existing cache data
-- Poor cache locality
-
-**Solution**: Implement exponential growth strategy with data preservation to minimize future reallocations.
-
-## 2. MicroVU Memory Allocation Patterns (MEDIUM PRIORITY)
-
-**Location**: `app/src/main/cpp/pcsx2/x86/microVU.cpp` lines 138-139, `microVU.h` lines 190, 163, 170
-
-**Issue**: Frequent `_aligned_malloc` and `_aligned_free` operations for microProgram structures and microBlockLink objects.
-
-**Code Patterns**:
-```cpp
-microProgram* prog = (microProgram*)_aligned_malloc(sizeof(microProgram), 64);
-microBlockLink* newBlock = (microBlockLink*)_aligned_malloc(sizeof(microBlockLink), 32);
-_aligned_free(freeI);
-```
-
-**Impact**: MEDIUM - Occurs during VU program creation/deletion:
-- Aligned memory allocation is more expensive than regular malloc
-- Frequent allocation/deallocation during emulation
-- Memory fragmentation from different alignment requirements
-
-**Solution**: Implement object pools or pre-allocated memory regions for these frequently used structures.
-
-## 3. Redundant Memory Clearing Operations (LOW-MEDIUM PRIORITY)
-
-**Location**: Multiple files with `std::memset` patterns
-
-**Issue**: Unnecessary zero-initialization of large structures, particularly:
-- `microVU_Branch.inl` lines 17-18: Clearing lpState structures
-- `iCore.cpp` lines 32, 930-933: Clearing register arrays
-- `microVU_Compile.inl`: Multiple memset operations
-
-**Code Patterns**:
-```cpp
-std::memset(µVU0.prog.lpState, 0, sizeof(microVU1.prog.lpState));
-std::memset(xmmregs, 0, sizeof(xmmregs));
-std::memset(pinst, 0, sizeof(EEINST));
-```
-
-**Impact**: LOW-MEDIUM - Cumulative effect across many operations:
-- Unnecessary CPU cycles spent zeroing memory
-- Some structures are immediately overwritten after clearing
-- Cache pollution from touching large memory regions
-
-**Solution**: Optimize initialization patterns and avoid redundant clears where data is immediately overwritten.
-
-## 4. ARM64 NEON SIMD Optimization Opportunities (MEDIUM PRIORITY)
-
-**Location**: `app/src/main/cpp/pcsx2/arm64/Vif_UnpackNEON.cpp`
-
-**Issue**: While the code already uses NEON instructions, there are opportunities for additional optimizations:
-
-**Current Implementation Analysis**:
-- VIF unpacking uses individual NEON operations
-- Some operations could be combined or vectorized further
-- Potential for better instruction scheduling
-
-**Code Example** (lines 294-295):
-```cpp
-armAsm->Shl(destReg.V4S(), destReg.V4S(), 24);
-armAsm->Ushr(destReg.V4S(), destReg.V4S(), 24);
-```
-
-**Impact**: MEDIUM - Affects graphics data processing performance:
-- VIF unpacking is on the critical path for graphics rendering
-- Better NEON utilization could improve frame rates
-- ARM64-specific optimizations not fully exploited
-
-**Solution**: Implement more efficient NEON instruction sequences and better utilize ARM64 capabilities.
-
-## 5. Loop Optimization Opportunities (LOW-MEDIUM PRIORITY)
-
-**Location**: Various files with for/while loops
-
-**Issue**: Some loops could benefit from unrolling or vectorization, particularly in:
-- Memory copying operations
-- Register clearing loops
-- Block iteration patterns
-
-**Examples**:
-- `BaseblockEx.cpp` lines 76-80: Simple iteration that could be unrolled
-- `microVU.h` lines 127-129, 253-255: Loops over fixed-size arrays
-
-**Impact**: LOW-MEDIUM - Depends on loop frequency:
-- Hot loops in recompilation paths could benefit from optimization
-- Some loops are over small, fixed-size arrays suitable for unrolling
-- Profile-guided optimization needed to identify highest impact loops
-
-**Solution**: Profile-guided optimization of hot loops with unrolling or vectorization where appropriate.
-
-## Performance Impact Assessment
-
-| Issue | Priority | Frequency | Impact per Operation | Overall Impact |
-|-------|----------|-----------|---------------------|----------------|
-| Instruction Cache Reallocation | HIGH | Every oversized block | High | HIGH |
-| MicroVU Memory Allocation | MEDIUM | VU program lifecycle | Medium | MEDIUM |
-| Redundant Memory Clearing | LOW-MEDIUM | Various operations | Low | LOW-MEDIUM |
-| NEON Optimizations | MEDIUM | Graphics processing | Medium | MEDIUM |
-| Loop Optimizations | LOW-MEDIUM | Various | Low-Medium | LOW-MEDIUM |
-
-## Recommendation
-
-**Immediate Action**: Implement the instruction cache optimization (Issue #1) as it has the highest impact and is straightforward to fix.
-
-**Future Work**: Address the MicroVU memory allocation patterns and explore additional NEON optimizations for graphics performance improvements.
-
-## Implementation Notes
-
-The instruction cache optimization should:
-1. Use exponential growth (doubling) to reduce future reallocations
-2. Preserve existing cache data during resize operations
-3. Maintain the same API and behavior
-4. Follow existing error handling patterns in the codebase
-
-This optimization will significantly reduce malloc/free overhead during block recompilation, which is a critical performance path in the emulator.
diff --git a/app/build.gradle b/app/build.gradle
index 946edb4..d07601b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,19 +3,19 @@ plugins {
}
android {
- namespace 'kr.co.iefriends.pcsx2'
+ namespace 'com.izzy2lost.psx2'
compileSdk 34
ndkVersion '27.0.12077973'
defaultConfig {
- applicationId "kr.co.iefriends.pcsx2"
+ applicationId "com.izzy2lost.psx2"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
// APK
- setProperty("archivesBaseName","PCSX2_${versionCode}_${new Date().format('yyyyMMddHHmm')}")
+ setProperty("archivesBaseName","PSX2_${versionCode}_${new Date().format('yyyyMMddHHmm')}")
externalNativeBuild {
cmake {
@@ -59,4 +59,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
+ implementation 'androidx.documentfile:documentfile:1.0.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b409b3b..7ad5fa3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,8 @@
android:installLocation="preferExternal"
tools:ignore="MissingLeanbackLauncher">
+
+
@@ -38,16 +40,16 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
- android:resizeableActivity="false"
- android:theme="@style/Theme.PCSX2"
+ android:theme="@style/Theme.PSX2"
tools:targetApi="31">
diff --git a/app/src/main/cpp/3rdparty/SDL3/src/core/android/SDL_android.c b/app/src/main/cpp/3rdparty/SDL3/src/core/android/SDL_android.c
index f33b022..691e392 100644
--- a/app/src/main/cpp/3rdparty/SDL3/src/core/android/SDL_android.c
+++ b/app/src/main/cpp/3rdparty/SDL3/src/core/android/SDL_android.c
@@ -45,7 +45,7 @@
#include
#include
-#define SDL_JAVA_PREFIX kr_co_iefriends_pcsx2
+#define SDL_JAVA_PREFIX com_izzy2lost_psx2
#define CONCAT1(prefix, class, function) CONCAT2(prefix, class, function)
#define CONCAT2(prefix, class, function) Java_##prefix##_##class##_##function
#define SDL_JAVA_INTERFACE(function) CONCAT1(SDL_JAVA_PREFIX, SDLActivity, function)
@@ -559,8 +559,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
// register_methods(env, "kr/co/iefriends/pcsx2/SDLActivity", SDLActivity_tab, SDL_arraysize(SDLActivity_tab));
// register_methods(env, "kr/co/iefriends/pcsx2/SDLInputConnection", SDLInputConnection_tab, SDL_arraysize(SDLInputConnection_tab));
// register_methods(env, "kr/co/iefriends/pcsx2/SDLAudioManager", SDLAudioManager_tab, SDL_arraysize(SDLAudioManager_tab));
- register_methods(env, "kr/co/iefriends/pcsx2/SDLControllerManager", SDLControllerManager_tab, SDL_arraysize(SDLControllerManager_tab));
- register_methods(env, "kr/co/iefriends/pcsx2/HIDDeviceManager", HIDDeviceManager_tab, SDL_arraysize(HIDDeviceManager_tab));
+ register_methods(env, "com/izzy2lost/psx2/SDLControllerManager", SDLControllerManager_tab, SDL_arraysize(SDLControllerManager_tab));
+ register_methods(env, "com/izzy2lost/psx2/HIDDeviceManager", HIDDeviceManager_tab, SDL_arraysize(HIDDeviceManager_tab));
return JNI_VERSION_1_4;
}
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
index a6581ab..3fad007 100644
--- a/app/src/main/cpp/native-lib.cpp
+++ b/app/src/main/cpp/native-lib.cpp
@@ -4,9 +4,11 @@
#include "PrecompiledHeader.h"
#include "common/StringUtil.h"
#include "common/FileSystem.h"
+#include "common/Error.h"
#include "common/ZipHelpers.h"
#include "pcsx2/GS.h"
#include "pcsx2/VMManager.h"
+#include "CDVD/CDVD.h"
#include "PerformanceMetrics.h"
#include "GameList.h"
#include "GS/GSPerfMon.h"
@@ -14,6 +16,7 @@
#include "ImGui/ImGuiManager.h"
#include "common/Path.h"
#include "common/MemorySettingsInterface.h"
+#include "pcsx2/INISettingsInterface.h"
#include "SIO/Pad/Pad.h"
#include "Input/InputManager.h"
#include "ImGui/ImGuiFullscreen.h"
@@ -32,6 +35,10 @@ int s_window_height = 0;
ANativeWindow* s_window = nullptr;
static MemorySettingsInterface s_settings_interface;
+static int s_pending_renderer = -1; // -1 = none; else 12=OpenGL,13=SW,14=Vulkan
+
+// Fallback JNI access for content:// when SDL's Android env is not yet ready
+// (no JNI fallback)
////
std::string GetJavaString(JNIEnv *env, jstring jstr) {
@@ -44,9 +51,122 @@ std::string GetJavaString(JNIEnv *env, jstring jstr) {
return cpp_string;
}
+static void ApplyPerGameSettingsForPath(const std::string& game_path)
+{
+ // Determine serial via CDVD using the same path the core will open
+ Error error;
+ std::string serial;
+ auto* prev = CDVD;
+ CDVD = &CDVDapi_Iso;
+ if (CDVD->open(game_path, &error))
+ {
+ (void)DoCDVDdetectDiskType();
+ cdvdGetDiscInfo(&serial, nullptr, nullptr, nullptr, nullptr);
+ DoCDVDclose();
+ }
+ CDVD = prev;
+
+ if (serial.empty())
+ return;
+
+ // Build settings path and load
+ const std::string settings_dir = Path::Combine(EmuFolders::DataRoot, "gamesettings");
+ const std::string settings_path = Path::Combine(settings_dir, serial + ".ini");
+ INISettingsInterface per_game(settings_path);
+ if (!per_game.Load())
+ return;
+
+ // Map known keys into our in-memory settings layer and apply where possible
+ std::string s;
+ float fval = 0.0f;
+ bool bval = false;
+
+ if (per_game.GetStringValue("EmuCore/GS", "Renderer", &s))
+ {
+ s_settings_interface.SetStringValue("EmuCore/GS", "Renderer", s.c_str());
+ // Defer actual renderer switch until VM is initialized
+ int rend = -1;
+ if (StringUtil::Strcasecmp(s.c_str(), "OpenGL") == 0) rend = 12;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Software") == 0) rend = 13;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Vulkan") == 0) rend = 14;
+ if (rend >= 0)
+ s_pending_renderer = rend;
+ }
+ if (per_game.GetFloatValue("EmuCore/GS", "upscale_multiplier", &fval))
+ s_settings_interface.SetFloatValue("EmuCore/GS", "upscale_multiplier", fval);
+ int abl_int = -1;
+ if (per_game.GetIntValue("EmuCore/GS", "accurate_blending_unit", &abl_int))
+ {
+ s_settings_interface.SetStringValue("EmuCore/GS", "accurate_blending_unit", StringUtil::StdStringFromFormat("%d", abl_int).c_str());
+ }
+ else if (per_game.GetStringValue("EmuCore/GS", "accurate_blending_unit", &s))
+ {
+ int lvl = 1;
+ if (StringUtil::Strcasecmp(s.c_str(), "Minimum") == 0) lvl = 0;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Basic") == 0) lvl = 1;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Medium") == 0) lvl = 2;
+ else if (StringUtil::Strcasecmp(s.c_str(), "High") == 0) lvl = 3;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Full") == 0) lvl = 4;
+ else if (StringUtil::Strcasecmp(s.c_str(), "Maximum") == 0) lvl = 5;
+ s_settings_interface.SetStringValue("EmuCore/GS", "accurate_blending_unit", StringUtil::StdStringFromFormat("%d", lvl).c_str());
+ }
+
+ if (per_game.GetBoolValue("EmuCore", "EnableWideScreenPatches", &bval))
+ s_settings_interface.SetBoolValue("EmuCore", "EnableWideScreenPatches", bval);
+ if (per_game.GetBoolValue("EmuCore", "EnableNoInterlacingPatches", &bval))
+ s_settings_interface.SetBoolValue("EmuCore", "EnableNoInterlacingPatches", bval);
+ if (per_game.GetBoolValue("EmuCore", "EnablePatches", &bval))
+ s_settings_interface.SetBoolValue("EmuCore", "EnablePatches", bval);
+ if (per_game.GetBoolValue("EmuCore", "EnableCheats", &bval))
+ s_settings_interface.SetBoolValue("EmuCore", "EnableCheats", bval);
+}
+
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setHudVisible(JNIEnv* env, jclass clazz, jboolean p_visible)
+{
+ const bool visible = (p_visible == JNI_TRUE);
+ MemorySettingsInterface& si = s_settings_interface;
+
+ // Toggle most HUD/OSD elements together
+ si.SetBoolValue("EmuCore/GS", "OsdShowSpeed", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowFPS", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVPS", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowCPU", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowGPU", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowResolution", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowGSStats", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowIndicators", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowSettings", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowInputs", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowFrameTimes", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVersion", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowHardwareInfo", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVideoCapture", visible);
+ si.SetBoolValue("EmuCore/GS", "OsdShowInputRec", visible);
+
+ // Apply changes to the running VM/renderer if active
+ VMManager::ApplySettings();
+ if (MTGS::IsOpen())
+ MTGS::ApplySettings();
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_setBlendingAccuracy(JNIEnv* env, jclass, jint level)
+{
+ // level: 0..5 -> numeric string
+ if (level < 0) level = 0; if (level > 5) level = 5;
+ s_settings_interface.SetStringValue("EmuCore/GS", "accurate_blending_unit", StringUtil::StdStringFromFormat("%d", level).c_str());
+ if (VMManager::HasValidVM())
+ VMManager::ApplySettings();
+ if (MTGS::IsOpen())
+ MTGS::ApplySettings();
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
jstring p_szpath, jint p_apiVer) {
std::string _szPath = GetJavaString(env, p_szpath);
EmuFolders::AppRoot = _szPath;
@@ -62,6 +182,11 @@ Java_kr_co_iefriends_pcsx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
MemorySettingsInterface &si = s_settings_interface;
Host::Internal::SetBaseSettingsLayer(&si);
+ // Initialize emulator folders and ensure they exist (including GameSettings)
+ EmuFolders::SetDefaults(si);
+ EmuFolders::LoadConfig(si);
+ EmuFolders::EnsureFoldersExist();
+
VMManager::SetDefaultSettings(si, true, true, true, true, true);
// complete as quickly as possible
@@ -85,10 +210,22 @@ Java_kr_co_iefriends_pcsx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
si.SetBoolValue("Logging", "EnableTimestamps", true);
si.SetBoolValue("Logging", "EnableVerbose", true);
- // and show some stats :)
- si.SetBoolValue("EmuCore/GS", "OsdShowFPS", true);
- si.SetBoolValue("EmuCore/GS", "OsdShowResolution", true);
- si.SetBoolValue("EmuCore/GS", "OsdShowGSStats", true);
+ // Default to a clean screen: hide HUD/OSD overlays by default
+ si.SetBoolValue("EmuCore/GS", "OsdShowSpeed", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowFPS", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVPS", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowCPU", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowGPU", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowResolution", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowGSStats", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowIndicators", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowSettings", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowInputs", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowFrameTimes", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVersion", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowHardwareInfo", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowVideoCapture", false);
+ si.SetBoolValue("EmuCore/GS", "OsdShowInputRec", false);
// // remove memory cards, so we don't have sharing violations
// for (u32 i = 0; i < 2; i++)
@@ -103,7 +240,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT jstring JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getGameTitle(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_getGameTitle(JNIEnv *env, jclass clazz,
jstring p_szpath) {
std::string _szPath = GetJavaString(env, p_szpath);
@@ -122,41 +259,88 @@ Java_kr_co_iefriends_pcsx2_NativeApp_getGameTitle(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT jstring JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getGameSerial(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_getCurrentGameSerial(JNIEnv *env, jclass clazz) {
std::string ret = VMManager::GetDiscSerial();
return env->NewStringUTF(ret.c_str());
}
extern "C"
JNIEXPORT jfloat JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getFPS(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_getFPS(JNIEnv *env, jclass clazz) {
return (jfloat)PerformanceMetrics::GetFPS();
}
extern "C"
JNIEXPORT jstring JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getPauseGameTitle(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_getPauseGameTitle(JNIEnv *env, jclass clazz) {
std::string ret = VMManager::GetTitle(true);
return env->NewStringUTF(ret.c_str());
}
extern "C"
JNIEXPORT jstring JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getPauseGameSerial(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_getPauseGameSerial(JNIEnv *env, jclass clazz) {
std::string ret = StringUtil::StdStringFromFormat("%s (%08X)", VMManager::GetDiscSerial().c_str(), VMManager::GetDiscCRC());
return env->NewStringUTF(ret.c_str());
}
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_izzy2lost_psx2_NativeApp_getGameSerial(JNIEnv* env, jclass, jstring p_uri)
+{
+ if (!p_uri)
+ return env->NewStringUTF("");
+ std::string path = GetJavaString(env, p_uri);
+
+ // Direct CDVD open to support content:// URIs for ISO/CHD
+ Error error;
+ std::string serial;
+ auto* prev = CDVD;
+ CDVD = &CDVDapi_Iso;
+ if (CDVD->open(path, &error))
+ {
+ (void)DoCDVDdetectDiskType();
+ cdvdGetDiscInfo(&serial, nullptr, nullptr, nullptr, nullptr);
+ DoCDVDclose();
+ }
+ CDVD = prev;
+ return env->NewStringUTF(serial.c_str());
+}
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_izzy2lost_psx2_NativeApp_getGameCrc(JNIEnv* env, jclass, jstring p_uri)
+{
+ if (!p_uri)
+ return env->NewStringUTF("");
+ std::string path = GetJavaString(env, p_uri);
+
+ Error error;
+ u32 crc = 0;
+ auto* prev = CDVD;
+ CDVD = &CDVDapi_Iso;
+ if (CDVD->open(path, &error))
+ {
+ (void)DoCDVDdetectDiskType();
+ cdvdGetDiscInfo(nullptr, nullptr, nullptr, &crc, nullptr);
+ DoCDVDclose();
+ }
+ CDVD = prev;
+
+ const std::string crc_hex = (crc != 0) ? StringUtil::StdStringFromFormat("%08X", crc) : std::string("");
+ return env->NewStringUTF(crc_hex.c_str());
+}
+
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_setPadVibration(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setPadVibration(JNIEnv *env, jclass clazz,
jboolean p_isOnOff) {
}
extern "C" JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_setPadButton(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setPadButton(JNIEnv *env, jclass clazz,
jint p_key, jint p_range, jboolean p_keyPressed) {
PadDualshock2::Inputs _key;
switch (p_key) {
@@ -191,66 +375,305 @@ Java_kr_co_iefriends_pcsx2_NativeApp_setPadButton(JNIEnv *env, jclass clazz,
}
extern "C" JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_resetKeyStatus(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_resetKeyStatus(JNIEnv *env, jclass clazz) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_setEnableCheats(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setEnableCheats(JNIEnv *env, jclass clazz,
jboolean p_isonoff) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_setAspectRatio(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setAspectRatio(JNIEnv *env, jclass clazz,
jint p_type) {
+ // AspectRatio values: 0=Stretch, 1=Auto 4:3/3:2, 2=4:3, 3=16:9, 4=10:7
+ const char* aspect_ratio_names[] = {
+ "Stretch",
+ "Auto 4:3/3:2",
+ "4:3",
+ "16:9",
+ "10:7"
+ };
+
+ if (p_type >= 0 && p_type < 5) {
+ s_settings_interface.SetStringValue("EmuCore/GS", "AspectRatio", aspect_ratio_names[p_type]);
+
+ // Apply settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
+ }
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_speedhackLimitermode(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_speedhackLimitermode(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_speedhackEecyclerate(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_speedhackEecyclerate(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_speedhackEecycleskip(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_speedhackEecycleskip(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_renderUpscalemultiplier(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_renderUpscalemultiplier(JNIEnv *env, jclass clazz,
jfloat p_value) {
+ if (p_value < 1.0f) p_value = 1.0f; // Ensure minimum 1x
+ if (p_value > 12.0f) p_value = 12.0f; // Cap at maximum 12x
+
+ s_settings_interface.SetFloatValue("EmuCore/GS", "upscale_multiplier", p_value);
+
+ // Apply the settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_renderMipmap(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_setWidescreenPatches(JNIEnv *env, jclass clazz,
+ jboolean p_enabled) {
+ s_settings_interface.SetBoolValue("EmuCore", "EnableWideScreenPatches", p_enabled);
+
+ // Apply the settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_setNoInterlacingPatches(JNIEnv *env, jclass clazz,
+ jboolean p_enabled) {
+ s_settings_interface.SetBoolValue("EmuCore", "EnableNoInterlacingPatches", p_enabled);
+
+ // Apply the settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_setLoadTextures(JNIEnv *env, jclass clazz,
+ jboolean p_enabled) {
+ s_settings_interface.SetBoolValue("EmuCore/GS", "LoadTextureReplacements", p_enabled);
+
+ // Apply the settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_setAsyncTextureLoading(JNIEnv *env, jclass clazz,
+ jboolean p_enabled) {
+ s_settings_interface.SetBoolValue("EmuCore/GS", "LoadTextureReplacementsAsync", p_enabled);
+
+ // Apply the settings immediately if emulation is running
+ if (VMManager::HasValidVM()) {
+ VMManager::ApplySettings();
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_saveGameSettings(JNIEnv *env, jclass clazz, jstring p_filename,
+ jint p_blending_accuracy, jint p_renderer,
+ jint p_resolution, jboolean p_widescreen_patches,
+ jboolean p_no_interlacing_patches, jboolean p_enable_patches,
+ jboolean p_enable_cheats)
+{
+ if (!p_filename)
+ return;
+
+ const char* filename_chars = env->GetStringUTFChars(p_filename, nullptr);
+ if (!filename_chars)
+ return;
+
+ // Use DataRoot directly for game settings to ensure write permissions
+ std::string settings_dir = Path::Combine(EmuFolders::DataRoot, "gamesettings");
+ std::string settings_path = Path::Combine(settings_dir, filename_chars);
+ env->ReleaseStringUTFChars(p_filename, filename_chars);
+
+ // Debug logging
+ printf("PCSX2: Saving game settings to: %s\n", settings_path.c_str());
+ printf("PCSX2: Settings directory: %s\n", settings_dir.c_str());
+ printf("PCSX2: Blending: %d, Renderer: %d, Resolution: %d\n", p_blending_accuracy, p_renderer, p_resolution);
+ printf("PCSX2: Widescreen: %d, NoInterlacing: %d, Patches: %d, Cheats: %d\n",
+ p_widescreen_patches, p_no_interlacing_patches, p_enable_patches, p_enable_cheats);
+
+ // Ensure directory exists
+ FileSystem::CreateDirectoryPath(settings_dir.c_str(), false);
+
+ // Build and write INI content directly to avoid any ambiguous formatting
+ const char* renderers[] = {"Auto", "Vulkan", "OpenGL", "Software"};
+
+ std::string ini;
+ ini.reserve(512);
+ ini += "[EmuCore/GS]\n";
+ // Renderer
+ if (p_renderer >= 0 && p_renderer < 4)
+ ini += std::string("Renderer=") + renderers[p_renderer] + "\n";
+ // Resolution scale (1..8)
+ if (p_resolution >= 0 && p_resolution <= 7)
+ {
+ float multiplier = 1.0f + (float)p_resolution;
+ ini += "upscale_multiplier=" + StringUtil::StdStringFromFormat("%.2f", multiplier) + "\n";
+ }
+ // Blending accuracy as numeric (0..5)
+ if (p_blending_accuracy >= 0 && p_blending_accuracy < 6)
+ ini += std::string("accurate_blending_unit=") + StringUtil::StdStringFromFormat("%d", p_blending_accuracy) + "\n";
+
+ ini += "\n[EmuCore]\n";
+ ini += std::string("EnableWideScreenPatches=") + (p_widescreen_patches ? "true" : "false") + "\n";
+ ini += std::string("EnableNoInterlacingPatches=") + (p_no_interlacing_patches ? "true" : "false") + "\n";
+ ini += std::string("EnablePatches=") + (p_enable_patches ? "true" : "false") + "\n";
+ ini += std::string("EnableCheats=") + (p_enable_cheats ? "true" : "false") + "\n";
+
+ const bool ok = FileSystem::WriteStringToFile(settings_path.c_str(), ini);
+ printf("PCSX2: Settings write %s: %s\n", ok ? "OK" : "FAILED", settings_path.c_str());
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_saveGameSettingsToPath(JNIEnv *env, jclass clazz, jstring p_full_path,
+ jint p_blending_accuracy, jint p_renderer,
+ jint p_resolution, jboolean p_widescreen_patches,
+ jboolean p_no_interlacing_patches, jboolean p_enable_patches,
+ jboolean p_enable_cheats)
+{
+ if (!p_full_path)
+ return;
+
+ const char* path_chars = env->GetStringUTFChars(p_full_path, nullptr);
+ if (!path_chars)
+ return;
+
+ std::string settings_path(path_chars);
+ env->ReleaseStringUTFChars(p_full_path, path_chars);
+
+ // Debug logging
+ printf("PCSX2: Saving game settings to full path: %s\n", settings_path.c_str());
+ printf("PCSX2: Blending: %d, Renderer: %d, Resolution: %d\n", p_blending_accuracy, p_renderer, p_resolution);
+ printf("PCSX2: Widescreen: %d, NoInterlacing: %d, Patches: %d, Cheats: %d\n",
+ p_widescreen_patches, p_no_interlacing_patches, p_enable_patches, p_enable_cheats);
+
+ // Ensure parent directory exists
+ std::string parent_dir(Path::GetDirectory(settings_path));
+ printf("PCSX2: Parent directory: %s\n", parent_dir.c_str());
+ bool dir_created = FileSystem::CreateDirectoryPath(parent_dir.c_str(), false);
+ printf("PCSX2: Directory creation result: %s\n", dir_created ? "SUCCESS" : "FAILED");
+
+ // Check if we can write to the directory
+ bool can_write = FileSystem::DirectoryExists(parent_dir.c_str());
+ printf("PCSX2: Directory exists: %s\n", can_write ? "YES" : "NO");
+
+ INISettingsInterface game_settings(settings_path);
+
+ // Blending accuracy (0=Minimum, 1=Basic, 2=Medium, 3=High, 4=Full, 5=Maximum)
+ const char* blend_levels[] = {"Minimum", "Basic", "Medium", "High", "Full", "Maximum"};
+ if (p_blending_accuracy >= 0 && p_blending_accuracy < 6) {
+ game_settings.SetStringValue("EmuCore/GS", "accurate_blending_unit", blend_levels[p_blending_accuracy]);
+ }
+
+ // Renderer (0=Auto, 1=Vulkan, 2=OpenGL, 3=Software)
+ const char* renderers[] = {"Auto", "Vulkan", "OpenGL", "Software"};
+ if (p_renderer >= 0 && p_renderer < 4) {
+ game_settings.SetStringValue("EmuCore/GS", "Renderer", renderers[p_renderer]);
+ }
+
+ // Resolution multiplier (same as global scale entries)
+ if (p_resolution >= 0 && p_resolution <= 7) {
+ float multiplier = 1.0f + (float)p_resolution;
+ game_settings.SetFloatValue("EmuCore/GS", "upscale_multiplier", multiplier);
+ }
+
+ // Patches
+ game_settings.SetBoolValue("EmuCore", "EnableWideScreenPatches", p_widescreen_patches);
+ game_settings.SetBoolValue("EmuCore", "EnableNoInterlacingPatches", p_no_interlacing_patches);
+ game_settings.SetBoolValue("EmuCore", "EnablePatches", p_enable_patches);
+ game_settings.SetBoolValue("EmuCore", "EnableCheats", p_enable_cheats);
+
+ // Test basic file write first
+ std::FILE* test_file = std::fopen(settings_path.c_str(), "w");
+ if (test_file) {
+ fprintf(test_file, "# Test file write\n");
+ std::fclose(test_file);
+ printf("PCSX2: Basic file write test: SUCCESS\n");
+ } else {
+ printf("PCSX2: Basic file write test: FAILED - errno: %d\n", errno);
+ return;
+ }
+
+ bool save_result = game_settings.Save();
+ printf("PCSX2: Settings save result: %s\n", save_result ? "SUCCESS" : "FAILED");
+
+ // Check if file actually exists and has content
+ if (FileSystem::FileExists(settings_path.c_str())) {
+ s64 file_size = FileSystem::GetPathFileSize(settings_path.c_str());
+ printf("PCSX2: File exists with size: %lld bytes\n", file_size);
+ } else {
+ printf("PCSX2: File does not exist after save attempt\n");
+ }
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_deleteGameSettings(JNIEnv *env, jclass clazz, jstring p_filename)
+{
+ if (!p_filename)
+ return;
+
+ const char* filename_chars = env->GetStringUTFChars(p_filename, nullptr);
+ if (!filename_chars)
+ return;
+
+ // Use DataRoot directly for game settings to ensure write permissions
+ std::string settings_dir = Path::Combine(EmuFolders::DataRoot, "gamesettings");
+ std::string settings_path = Path::Combine(settings_dir, filename_chars);
+ env->ReleaseStringUTFChars(p_filename, filename_chars);
+
+ if (FileSystem::FileExists(settings_path.c_str())) {
+ FileSystem::DeleteFilePath(settings_path.c_str());
+ }
+}
+
+
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_izzy2lost_psx2_NativeApp_renderMipmap(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_renderHalfpixeloffset(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_renderHalfpixeloffset(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_renderPreloading(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_renderPreloading(JNIEnv *env, jclass clazz,
jint p_value) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_renderGpu(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_renderGpu(JNIEnv *env, jclass clazz,
jint p_value) {
EmuConfig.GS.Renderer = static_cast(p_value);
if(MTGS::IsOpen()) {
@@ -260,12 +683,12 @@ Java_kr_co_iefriends_pcsx2_NativeApp_renderGpu(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_onNativeSurfaceCreated(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_onNativeSurfaceCreated(JNIEnv *env, jclass clazz) {
}
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_onNativeSurfaceChanged(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_onNativeSurfaceChanged(JNIEnv *env, jclass clazz,
jobject p_surface, jint p_width, jint p_height) {
if(s_window) {
ANativeWindow_release(s_window);
@@ -287,7 +710,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_onNativeSurfaceChanged(JNIEnv *env, jclass
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_onNativeSurfaceDestroyed(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_onNativeSurfaceDestroyed(JNIEnv *env, jclass clazz) {
if(s_window) {
ANativeWindow_release(s_window);
s_window = nullptr;
@@ -388,7 +811,7 @@ int FileSystem::OpenFDFileContent(const char* filename)
if(env == nullptr) {
return -1;
}
- jclass NativeApp = env->FindClass("kr/co/iefriends/pcsx2/NativeApp");
+ jclass NativeApp = env->FindClass("com/izzy2lost/psx2/NativeApp");
jmethodID openContentUri = env->GetStaticMethodID(NativeApp, "openContentUri", "(Ljava/lang/String;)I");
jstring j_filename = env->NewStringUTF(filename);
@@ -399,7 +822,7 @@ int FileSystem::OpenFDFileContent(const char* filename)
extern "C"
JNIEXPORT jboolean JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_runVMThread(JNIEnv *env, jclass clazz,
+Java_com_izzy2lost_psx2_NativeApp_runVMThread(JNIEnv *env, jclass clazz,
jstring p_szpath) {
std::string _szPath = GetJavaString(env, p_szpath);
@@ -416,6 +839,9 @@ Java_kr_co_iefriends_pcsx2_NativeApp_runVMThread(JNIEnv *env, jclass clazz,
VMBootParameters boot_params;
boot_params.filename = _szPath;
+ // Apply per-game settings (if any) before applying core settings
+ ApplyPerGameSettingsForPath(_szPath);
+
if (!VMManager::Internal::CPUThreadInitialize()) {
VMManager::Internal::CPUThreadShutdown();
}
@@ -425,6 +851,14 @@ Java_kr_co_iefriends_pcsx2_NativeApp_runVMThread(JNIEnv *env, jclass clazz,
if (VMManager::Initialize(boot_params))
{
+ // If a per-game renderer was requested, apply it now that VM is up.
+ if (s_pending_renderer >= 0)
+ {
+ EmuConfig.GS.Renderer = static_cast(s_pending_renderer);
+ s_pending_renderer = -1;
+ if (MTGS::IsOpen())
+ MTGS::ApplySettings();
+ }
VMState _vmState = VMState::Running;
VMManager::SetState(_vmState);
////
@@ -451,7 +885,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_runVMThread(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_pause(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_pause(JNIEnv *env, jclass clazz) {
std::thread([] {
VMManager::SetPaused(true);
}).detach();
@@ -459,7 +893,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_pause(JNIEnv *env, jclass clazz) {
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_resume(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_resume(JNIEnv *env, jclass clazz) {
std::thread([] {
VMManager::SetPaused(false);
}).detach();
@@ -467,7 +901,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_resume(JNIEnv *env, jclass clazz) {
extern "C"
JNIEXPORT void JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_shutdown(JNIEnv *env, jclass clazz) {
+Java_com_izzy2lost_psx2_NativeApp_shutdown(JNIEnv *env, jclass clazz) {
std::thread([] {
VMManager::SetState(VMState::Stopping);
}).detach();
@@ -476,7 +910,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_shutdown(JNIEnv *env, jclass clazz) {
extern "C"
JNIEXPORT jboolean JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_saveStateToSlot(JNIEnv *env, jclass clazz, jint p_slot) {
+Java_com_izzy2lost_psx2_NativeApp_saveStateToSlot(JNIEnv *env, jclass clazz, jint p_slot) {
if (!VMManager::HasValidVM()) {
return false;
}
@@ -508,7 +942,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_saveStateToSlot(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT jboolean JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_loadStateFromSlot(JNIEnv *env, jclass clazz, jint p_slot) {
+Java_com_izzy2lost_psx2_NativeApp_loadStateFromSlot(JNIEnv *env, jclass clazz, jint p_slot) {
if (!VMManager::HasValidVM()) {
return false;
}
@@ -542,7 +976,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_loadStateFromSlot(JNIEnv *env, jclass clazz
extern "C"
JNIEXPORT jstring JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getGamePathSlot(JNIEnv *env, jclass clazz, jint p_slot) {
+Java_com_izzy2lost_psx2_NativeApp_getGamePathSlot(JNIEnv *env, jclass clazz, jint p_slot) {
std::string _filename = VMManager::GetSaveStateFileName(VMManager::GetDiscSerial().c_str(), VMManager::GetDiscCRC(), p_slot);
if(!_filename.empty()) {
return env->NewStringUTF(_filename.c_str());
@@ -552,7 +986,7 @@ Java_kr_co_iefriends_pcsx2_NativeApp_getGamePathSlot(JNIEnv *env, jclass clazz,
extern "C"
JNIEXPORT jbyteArray JNICALL
-Java_kr_co_iefriends_pcsx2_NativeApp_getImageSlot(JNIEnv *env, jclass clazz, jint p_slot) {
+Java_com_izzy2lost_psx2_NativeApp_getImageSlot(JNIEnv *env, jclass clazz, jint p_slot) {
jbyteArray retArr = nullptr;
std::string _filename = VMManager::GetSaveStateFileName(VMManager::GetDiscSerial().c_str(), VMManager::GetDiscCRC(), p_slot);
diff --git a/app/src/main/cpp/pcsx2/CDVD/ChdFileReader.cpp b/app/src/main/cpp/pcsx2/CDVD/ChdFileReader.cpp
index 20033ee..0c929a1 100644
--- a/app/src/main/cpp/pcsx2/CDVD/ChdFileReader.cpp
+++ b/app/src/main/cpp/pcsx2/CDVD/ChdFileReader.cpp
@@ -16,6 +16,10 @@
#include "fmt/format.h"
#include "xxhash.h"
+#ifndef _WIN32
+#include
+#endif
+
static constexpr u32 MAX_PARENTS = 32; // Surely someone wouldn't be insane enough to go beyond this...
static std::vector> s_chd_hash_cache; //
static std::recursive_mutex s_chd_hash_cache_mutex;
@@ -366,17 +370,43 @@ static chd_file* OpenCHD(const std::string& filename, FileSystem::ManagedCFilePt
bool ChdFileReader::Open2(std::string filename, Error* error)
{
- Close2();
+ Close2();
- m_filename = std::move(filename);
+ m_filename = std::move(filename);
- auto fp = FileSystem::OpenManagedSharedCFile(m_filename.c_str(), "rb", FileSystem::FileShareMode::DenyWrite, error);
- if (!fp)
- return false;
+ FileSystem::ManagedCFilePtr fp;
+ // Support Android Storage Access Framework content URIs (e.g., from the picker)
+ if (m_filename.rfind("content://", 0) == 0)
+ {
+ const int fd = FileSystem::OpenFDFileContent(m_filename.c_str());
+ if (fd < 0)
+ {
+ Error::SetStringView(error, "Failed to open CHD content URI.");
+ return false;
+ }
- ChdFile = OpenCHD(m_filename, std::move(fp), error, 0);
- if (!ChdFile)
- return false;
+ std::FILE* f = fdopen(fd, "rb");
+ if (!f)
+ {
+#ifndef _WIN32
+ close(fd);
+#endif
+ Error::SetStringView(error, "Failed to create file stream for CHD content URI.");
+ return false;
+ }
+
+ fp.reset(f);
+ }
+ else
+ {
+ fp = FileSystem::OpenManagedSharedCFile(m_filename.c_str(), "rb", FileSystem::FileShareMode::DenyWrite, error);
+ if (!fp)
+ return false;
+ }
+
+ ChdFile = OpenCHD(m_filename, std::move(fp), error, 0);
+ if (!ChdFile)
+ return false;
const chd_header* chd_header = chd_get_header(ChdFile);
hunk_size = chd_header->hunkbytes;
diff --git a/app/src/main/cpp/pcsx2/x86/microVU.cpp b/app/src/main/cpp/pcsx2/x86/microVU.cpp
index e6cf3df..23ac7d8 100644
--- a/app/src/main/cpp/pcsx2/x86/microVU.cpp
+++ b/app/src/main/cpp/pcsx2/x86/microVU.cpp
@@ -122,24 +122,46 @@ __fi void mVUclear(mV, u32 addr, u32 size)
//------------------------------------------------------------------
// Deletes a program
+// Simple reuse pool for microProgram allocations to avoid frequent aligned malloc/free.
+static std::vector s_microprog_pool;
+static constexpr size_t kMicroProgPoolMax = 128;
+
__ri void mVUdeleteProg(microVU& mVU, microProgram*& prog)
{
u32 i, e = (mVU.progSize >> 1); // mVU.progSize / 2
- for (i = 0; i < e; ++i)
- {
- safe_delete(prog->block[i]);
- }
- safe_delete(prog->ranges);
- safe_aligned_free(prog);
+ for (i = 0; i < e; ++i)
+ {
+ safe_delete(prog->block[i]);
+ }
+ safe_delete(prog->ranges);
+ // Reuse the microProgram object to reduce allocator overhead.
+ if (s_microprog_pool.size() < kMicroProgPoolMax)
+ {
+ s_microprog_pool.push_back(prog);
+ }
+ else
+ {
+ safe_aligned_free(prog);
+ }
}
// Creates a new Micro Program
__ri microProgram* mVUcreateProg(microVU& mVU, int startPC)
{
- auto* prog = (microProgram*)_aligned_malloc(sizeof(microProgram), 64);
- memset(prog, 0, sizeof(microProgram));
- prog->idx = mVU.prog.total++;
- prog->ranges = new std::deque();
+ microProgram* prog = nullptr;
+ if (!s_microprog_pool.empty())
+ {
+ prog = s_microprog_pool.back();
+ s_microprog_pool.pop_back();
+ std::memset(prog, 0, sizeof(microProgram));
+ }
+ else
+ {
+ prog = (microProgram*)_aligned_malloc(sizeof(microProgram), 64);
+ std::memset(prog, 0, sizeof(microProgram));
+ }
+ prog->idx = mVU.prog.total++;
+ prog->ranges = new std::deque();
prog->startPC = startPC;
if(doWholeProgCompare)
mVUcacheProg(mVU, *prog); // Cache Micro Program
diff --git a/app/src/main/cpp/pcsx2/x86/microVU.h b/app/src/main/cpp/pcsx2/x86/microVU.h
index 4441cf8..1c9cb80 100644
--- a/app/src/main/cpp/pcsx2/x86/microVU.h
+++ b/app/src/main/cpp/pcsx2/x86/microVU.h
@@ -26,10 +26,11 @@ class microBlockManager;
struct microBlockLink
{
- microBlock block;
- microBlockLink* next;
+ microBlock block;
+ microBlockLink* next;
};
+
struct microBlockLinkRef
{
microBlock* pBlock;
@@ -146,6 +147,37 @@ private:
std::vector quickLookup;
int qListI, fListI;
+ // Simple free-list pool for microBlockLink to reduce aligned allocations.
+ static microBlockLink* s_free_links;
+ static int s_free_link_count;
+ static constexpr int s_free_link_max = 4096;
+
+ static microBlockLink* allocLink()
+ {
+ if (s_free_links)
+ {
+ microBlockLink* p = s_free_links;
+ s_free_links = s_free_links->next;
+ s_free_link_count--;
+ return p;
+ }
+ return (microBlockLink*)_aligned_malloc(sizeof(microBlockLink), 32);
+ }
+ static void freeLink(microBlockLink* p)
+ {
+ if (!p) return;
+ if (s_free_link_count < s_free_link_max)
+ {
+ p->next = s_free_links;
+ s_free_links = p;
+ s_free_link_count++;
+ }
+ else
+ {
+ _aligned_free(p);
+ }
+ }
+
public:
inline int getFullListCount() const { return fListI; }
microBlockManager()
@@ -162,14 +194,14 @@ public:
microBlockLink* freeI = linkI;
safe_delete_array(linkI->block.jumpCache);
linkI = linkI->next;
- _aligned_free(freeI);
+ freeLink(freeI);
}
for (microBlockLink* linkI = fBlockList; linkI != nullptr;)
{
microBlockLink* freeI = linkI;
safe_delete_array(linkI->block.jumpCache);
linkI = linkI->next;
- _aligned_free(freeI);
+ freeLink(freeI);
}
qListI = fListI = 0;
qBlockEnd = qBlockList = nullptr;
@@ -189,7 +221,7 @@ public:
microBlockLink*& blockList = fullCmp ? fBlockList : qBlockList;
microBlockLink*& blockEnd = fullCmp ? fBlockEnd : qBlockEnd;
- microBlockLink* newBlock = (microBlockLink*)_aligned_malloc(sizeof(microBlockLink), 32);
+ microBlockLink* newBlock = allocLink();
newBlock->block.jumpCache = nullptr;
newBlock->next = nullptr;
@@ -266,9 +298,12 @@ public:
linkI = linkI->next;
}
}
-};
+ };
+// Static members initialization (after class definition to avoid incomplete-type errors)
+inline microBlockLink* microBlockManager::s_free_links = nullptr;
+inline int microBlockManager::s_free_link_count = 0;
-// microVU rec structs
+ // microVU rec structs
//alignas(16) microVU microVU0;
//alignas(16) microVU microVU1;
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..05a0084
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java b/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java
new file mode 100644
index 0000000..2b41f6a
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java
@@ -0,0 +1,98 @@
+package com.izzy2lost.psx2;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import java.io.File;
+
+public class CoversAdapter extends RecyclerView.Adapter {
+ public interface OnItemClick {
+ void onClick(int position);
+ }
+
+ public interface OnItemLongClick {
+ void onLongClick(int position);
+ }
+
+ private final Context context;
+ private final String[] titles;
+ private final String[] coverUrls;
+ private final String[] localPaths; // absolute file paths for cached covers (may be null)
+ private final OnItemClick onItemClick;
+ private final OnItemLongClick onItemLongClick;
+
+ public CoversAdapter(Context context, String[] titles, String[] coverUrls, String[] localPaths, OnItemClick click) {
+ this(context, titles, coverUrls, localPaths, click, null);
+ }
+
+ public CoversAdapter(Context context, String[] titles, String[] coverUrls, String[] localPaths, OnItemClick click, OnItemLongClick longClick) {
+ this.context = context;
+ this.titles = titles;
+ this.coverUrls = coverUrls;
+ this.localPaths = localPaths;
+ this.onItemClick = click;
+ this.onItemLongClick = longClick;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_cover, parent, false);
+ return new VH(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ holder.title.setText(titles[position]);
+ String url = coverUrls[position];
+ String local = (localPaths != null && position < localPaths.length) ? localPaths[position] : null;
+ Object source = null;
+ if (local != null) {
+ File f = new File(local);
+ if (f.exists() && f.length() > 0) source = f;
+ }
+ if (source == null) source = url;
+
+ Glide.with(context)
+ .load(source)
+ .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
+ .fitCenter()
+ .placeholder(android.R.color.transparent)
+ .error(android.R.color.transparent)
+ .into(holder.cover);
+ holder.itemView.setOnClickListener(v -> {
+ if (onItemClick != null) onItemClick.onClick(position);
+ });
+ holder.itemView.setOnLongClickListener(v -> {
+ if (onItemLongClick != null) {
+ onItemLongClick.onLongClick(position);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return titles.length;
+ }
+
+ static class VH extends RecyclerView.ViewHolder {
+ final ImageView cover;
+ final TextView title;
+ VH(@NonNull View itemView) {
+ super(itemView);
+ cover = itemView.findViewById(R.id.image_cover);
+ title = itemView.findViewById(R.id.text_title);
+ }
+ }
+}
diff --git a/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java
new file mode 100644
index 0000000..e8e93e7
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java
@@ -0,0 +1,259 @@
+package com.izzy2lost.psx2;
+
+import android.app.Dialog;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+public class GameSettingsDialogFragment extends DialogFragment {
+
+ private static final String ARG_GAME_TITLE = "game_title";
+ private static final String ARG_GAME_URI = "game_uri";
+ private static final String ARG_GAME_SERIAL = "game_serial";
+ private static final String ARG_GAME_CRC = "game_crc";
+
+ public static GameSettingsDialogFragment newInstance(String gameTitle, String gameUri, String gameSerial, String gameCrc) {
+ GameSettingsDialogFragment fragment = new GameSettingsDialogFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_GAME_TITLE, gameTitle);
+ args.putString(ARG_GAME_URI, gameUri);
+ args.putString(ARG_GAME_SERIAL, gameSerial);
+ args.putString(ARG_GAME_CRC, gameCrc);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ Context ctx = requireContext();
+ View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_game_settings, null, false);
+
+ Bundle args = getArguments();
+ String gameTitle = args != null ? args.getString(ARG_GAME_TITLE, "Unknown Game") : "Unknown Game";
+ String gameUri = args != null ? args.getString(ARG_GAME_URI, "") : "";
+ String gameSerial = args != null ? args.getString(ARG_GAME_SERIAL, "") : "";
+ String gameCrc = args != null ? args.getString(ARG_GAME_CRC, "") : "";
+
+ // Set title
+ TextView titleView = view.findViewById(R.id.tv_game_title);
+ titleView.setText(gameTitle);
+
+ TextView serialView = view.findViewById(R.id.tv_game_serial);
+ if (!gameSerial.isEmpty() || !gameCrc.isEmpty()) {
+ serialView.setText(String.format("Serial: %s | CRC: %s",
+ gameSerial.isEmpty() ? "Unknown" : gameSerial,
+ gameCrc.isEmpty() ? "Unknown" : gameCrc));
+ serialView.setVisibility(View.VISIBLE);
+ } else {
+ serialView.setVisibility(View.GONE);
+ }
+
+ // Blending Accuracy Spinner
+ Spinner spBlendingAccuracy = view.findViewById(R.id.sp_blending_accuracy);
+ ArrayAdapter blendingAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.blending_accuracy_entries, android.R.layout.simple_spinner_item);
+ blendingAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spBlendingAccuracy.setAdapter(blendingAdapter);
+
+ // Renderer Spinner
+ Spinner spRenderer = view.findViewById(R.id.sp_renderer);
+ ArrayAdapter rendererAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.renderer_entries, android.R.layout.simple_spinner_item);
+ rendererAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spRenderer.setAdapter(rendererAdapter);
+
+ // Resolution Multiplier Spinner
+ Spinner spResolution = view.findViewById(R.id.sp_resolution);
+ ArrayAdapter resolutionAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.scale_entries, android.R.layout.simple_spinner_item);
+ resolutionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spResolution.setAdapter(resolutionAdapter);
+
+ // Switches
+ Switch swWidescreenPatches = view.findViewById(R.id.sw_widescreen_patches);
+ Switch swNoInterlacingPatches = view.findViewById(R.id.sw_no_interlacing_patches);
+ Switch swEnablePatchCodes = view.findViewById(R.id.sw_enable_patch_codes);
+ Switch swEnableCheats = view.findViewById(R.id.sw_enable_cheats);
+
+ // Load existing per-game settings from INI and prefill widgets
+ try {
+ String serial = gameSerial;
+ if (serial == null || serial.isEmpty()) {
+ serial = NativeApp.getCurrentGameSerial();
+ }
+ if (serial != null && !serial.isEmpty()) {
+ // Build INI path
+ String dataRoot = getContext().getExternalFilesDir(null).getAbsolutePath();
+ java.io.File ini = new java.io.File(new java.io.File(dataRoot, "gamesettings"), serial + ".ini");
+ if (ini.exists()) {
+ String content = new String(java.nio.file.Files.readAllBytes(ini.toPath()));
+ // Very light parsing
+ java.util.regex.Matcher m;
+ m = java.util.regex.Pattern.compile("(?m)^Renderer=\\s*(.+)$").matcher(content);
+ if (m.find()) {
+ String rv = m.group(1).trim();
+ int idx = 0;
+ if ("Vulkan".equalsIgnoreCase(rv)) idx = 1;
+ else if ("OpenGL".equalsIgnoreCase(rv)) idx = 2;
+ else if ("Software".equalsIgnoreCase(rv)) idx = 3;
+ spRenderer.setSelection(idx);
+ }
+ m = java.util.regex.Pattern.compile("(?m)^upscale_multiplier=\\s*([0-9]+(?:\\.[0-9]+)?)$").matcher(content);
+ if (m.find()) {
+ try { float mult = Float.parseFloat(m.group(1)); int sel = Math.max(0, Math.min(7, Math.round(mult - 1))); spResolution.setSelection(sel); } catch (Exception ignored) {}
+ }
+ m = java.util.regex.Pattern.compile("(?m)^accurate_blending_unit=\\s*(.+)$").matcher(content);
+ if (m.find()) {
+ String bv = m.group(1).trim();
+ int idx = 1; // default Basic
+ try {
+ int num = Integer.parseInt(bv);
+ if (num >= 0 && num <= 5) idx = num;
+ } catch (Exception e) {
+ if ("Minimum".equalsIgnoreCase(bv)) idx = 0;
+ else if ("Basic".equalsIgnoreCase(bv)) idx = 1;
+ else if ("Medium".equalsIgnoreCase(bv)) idx = 2;
+ else if ("High".equalsIgnoreCase(bv)) idx = 3;
+ else if ("Full".equalsIgnoreCase(bv)) idx = 4;
+ else if ("Maximum".equalsIgnoreCase(bv)) idx = 5;
+ }
+ spBlendingAccuracy.setSelection(idx);
+ }
+ m = java.util.regex.Pattern.compile("(?m)^EnableWideScreenPatches=\\s*(true|false)$").matcher(content);
+ if (m.find()) swWidescreenPatches.setChecked(Boolean.parseBoolean(m.group(1)));
+ m = java.util.regex.Pattern.compile("(?m)^EnableNoInterlacingPatches=\\s*(true|false)$").matcher(content);
+ if (m.find()) swNoInterlacingPatches.setChecked(Boolean.parseBoolean(m.group(1)));
+ m = java.util.regex.Pattern.compile("(?m)^EnableCheats=\\s*(true|false)$").matcher(content);
+ if (m.find()) swEnableCheats.setChecked(Boolean.parseBoolean(m.group(1)));
+ m = java.util.regex.Pattern.compile("(?m)^EnablePatches=\\s*(true|false)$").matcher(content);
+ if (m.find()) swEnablePatchCodes.setChecked(Boolean.parseBoolean(m.group(1)));
+ }
+ }
+ } catch (Throwable ignored) {
+ // Fallback to defaults if loading fails
+ spBlendingAccuracy.setSelection(1);
+ spRenderer.setSelection(0);
+ spResolution.setSelection(0);
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle("Per-Game Settings")
+ .setView(view)
+ .setNegativeButton("Cancel", (d, w) -> d.dismiss())
+ .setPositiveButton("Save", (d, w) -> {
+ // Apply blending to runtime as well for immediate effect
+ NativeApp.setBlendingAccuracy(spBlendingAccuracy.getSelectedItemPosition());
+
+ saveGameSettings(gameSerial, gameCrc,
+ spBlendingAccuracy.getSelectedItemPosition(),
+ spRenderer.getSelectedItemPosition(),
+ spResolution.getSelectedItemPosition(),
+ swWidescreenPatches.isChecked(),
+ swNoInterlacingPatches.isChecked(),
+ /*enablePatches*/ swEnablePatchCodes.isChecked(),
+ swEnableCheats.isChecked());
+ d.dismiss();
+ })
+ .setNeutralButton("Reset to Global", (d, w) -> {
+ // TODO: Delete game-specific settings file
+ deleteGameSettings(gameSerial, gameCrc);
+ d.dismiss();
+ });
+
+ // Import PNACH button wiring
+ com.google.android.material.button.MaterialButton btnImport = view.findViewById(R.id.btn_import_pnach);
+ if (btnImport != null) {
+ btnImport.setOnClickListener(v -> {
+ final String[] choices = new String[]{"Import as Cheats", "Import as Patch Codes"};
+ new AlertDialog.Builder(ctx)
+ .setTitle("Import PNACH")
+ .setItems(choices, (dlg, which) -> {
+ boolean asCheats = (which == 0);
+ // Prepare picker
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ // Store choice in tag
+ view.setTag(R.id.btn_import_pnach, asCheats);
+ registerForActivityResult(new androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(), result -> {
+ try {
+ if (result.getResultCode() != android.app.Activity.RESULT_OK) return;
+ Intent data = result.getData(); if (data == null) return;
+ android.net.Uri uri = data.getData(); if (uri == null) return;
+ boolean importAsCheats = Boolean.TRUE.equals(view.getTag(R.id.btn_import_pnach));
+ String serialLoad = gameSerial;
+ if (serialLoad == null || serialLoad.isEmpty()) {
+ try { serialLoad = NativeApp.getCurrentGameSerial(); } catch (Throwable ignored) {}
+ }
+ if (serialLoad == null || serialLoad.isEmpty()) {
+ android.widget.Toast.makeText(ctx, "Serial unknown; cannot import", android.widget.Toast.LENGTH_SHORT).show();
+ return;
+ }
+ java.io.File baseDir = ctx.getExternalFilesDir(null);
+ if (baseDir == null) baseDir = ctx.getFilesDir();
+ java.io.File targetDir = new java.io.File(baseDir, importAsCheats ? "cheats" : "patches");
+ if (!targetDir.exists()) targetDir.mkdirs();
+ java.io.File outFile = new java.io.File(targetDir, serialLoad + ".pnach");
+ android.content.ContentResolver cr = ctx.getContentResolver();
+ java.io.InputStream in = cr.openInputStream(uri);
+ if (in == null) { android.widget.Toast.makeText(ctx, "Failed to open file", android.widget.Toast.LENGTH_SHORT).show(); return; }
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(outFile);
+ byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) != -1) fos.write(buf, 0, n);
+ fos.flush(); fos.close(); in.close();
+ android.widget.Toast.makeText(ctx, (importAsCheats ? "Cheats" : "Patch Codes") + " imported for " + serialLoad, android.widget.Toast.LENGTH_SHORT).show();
+ } catch (Exception e) {
+ android.widget.Toast.makeText(ctx, "Import failed: " + e.getMessage(), android.widget.Toast.LENGTH_SHORT).show();
+ }
+ }).launch(intent);
+ })
+ .show();
+ });
+ }
+
+ return builder.create();
+ }
+
+ private void saveGameSettings(String gameSerial, String gameCrc,
+ int blendingAccuracy, int renderer, int resolution,
+ boolean widescreenPatches, boolean noInterlacingPatches,
+ boolean enablePatches, boolean enableCheats) {
+
+ // Create the settings filename based on serial (like SLUS-12345.ini)
+ if (gameSerial == null || gameSerial.isEmpty()) {
+ android.util.Log.w("GameSettings", "No game serial available, cannot save settings");
+ return;
+ }
+ String filename = gameSerial + ".ini";
+
+ // Save to PCSX2's DataRoot/gamesettings via native helper.
+ NativeApp.saveGameSettings(filename, blendingAccuracy, renderer, resolution,
+ widescreenPatches, noInterlacingPatches, enablePatches, enableCheats);
+
+ android.widget.Toast.makeText(requireContext(), "Game settings saved: " + filename, android.widget.Toast.LENGTH_SHORT).show();
+ }
+
+ private void deleteGameSettings(String gameSerial, String gameCrc) {
+ String filename = "";
+ if (!gameSerial.isEmpty()) {
+ filename = gameSerial + ".ini";
+ } else {
+ return;
+ }
+
+ NativeApp.deleteGameSettings(filename);
+ }
+}
diff --git a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java
new file mode 100644
index 0000000..695f2e4
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java
@@ -0,0 +1,388 @@
+package com.izzy2lost.psx2;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.net.Uri;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+import android.os.Build;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class GamesCoverDialogFragment extends DialogFragment {
+ private CoversAdapter adapter;
+ private String[] titles;
+ private String[] uris;
+ private String[] coverUrls;
+ private String[] localPaths;
+ private RecyclerView rv;
+ private GridLayoutManager glm;
+
+ public interface OnGameSelectedListener {
+ void onGameSelected(String gameUri);
+ }
+
+ private static final String ARG_TITLES = "titles";
+ private static final String ARG_URIS = "uris";
+
+ public static GamesCoverDialogFragment newInstance(String[] titles, String[] uris) {
+ GamesCoverDialogFragment f = new GamesCoverDialogFragment();
+ Bundle b = new Bundle();
+ b.putStringArray(ARG_TITLES, titles);
+ b.putStringArray(ARG_URIS, uris);
+ f.setArguments(b);
+ return f;
+ }
+
+ private OnGameSelectedListener listener;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof OnGameSelectedListener) {
+ listener = (OnGameSelectedListener) context;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Re-assert fixed span (2/4) after resume to avoid any flips
+ if (rv != null && glm != null) {
+ int currentOrientation = getResources().getConfiguration().orientation;
+ int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
+ if (fixedSpan != glm.getSpanCount()) {
+ glm.setSpanCount(fixedSpan);
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ LayoutInflater inflater = LayoutInflater.from(requireContext());
+ View root = inflater.inflate(R.layout.dialog_covers_grid, null, false);
+
+ rv = root.findViewById(R.id.recycler_covers);
+ rv.setHasFixedSize(true);
+ glm = new GridLayoutManager(requireContext(), 3);
+ rv.setLayoutManager(glm);
+ // spacing decoration (8dp) using half on each side so the gap between items is exactly spacingPx
+ final int spacingPx = (int) (8 * getResources().getDisplayMetrics().density);
+ final int half = Math.max(1, spacingPx / 2);
+ rv.addItemDecoration(new RecyclerView.ItemDecoration() {
+ @Override
+ public void getItemOffsets(android.graphics.Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.set(half, half, half, half);
+ }
+ });
+ // Hard lock spans based on orientation only
+ rv.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ int currentOrientation = getResources().getConfiguration().orientation;
+ int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
+ if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); }
+ });
+ // Set initial fixed span as soon as possible
+ root.post(() -> {
+ int currentOrientation = getResources().getConfiguration().orientation;
+ int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2;
+ if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); }
+ });
+
+ titles = getArguments() != null ? getArguments().getStringArray(ARG_TITLES) : new String[0];
+ uris = getArguments() != null ? getArguments().getStringArray(ARG_URIS) : new String[0];
+ coverUrls = new String[uris.length];
+ localPaths = new String[uris.length];
+ SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
+ for (int i = 0; i < uris.length; i++) {
+ String saved = prefs.getString("serial:" + uris[i], null);
+ String serial = saved;
+ if (serial == null || serial.isEmpty()) {
+ // Ask native core for the real serial (supports ISO/CHD and content://)
+ try {
+ String nativeSerial = NativeApp.getGameSerial(uris[i]);
+ if (nativeSerial != null && !nativeSerial.isEmpty()) {
+ serial = normalizeSerial(nativeSerial);
+ prefs.edit().putString("serial:" + uris[i], serial).apply();
+ }
+ } catch (Throwable ignored) {}
+ }
+ if (serial == null || serial.isEmpty()) {
+ // Heuristic fallback from filename
+ serial = buildSerialFromUri(uris[i]);
+ }
+ coverUrls[i] = buildCoverUrlFromSerial(serial);
+ localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
+ }
+
+ adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths,
+ position -> {
+ // Regular click - start game
+ if (listener != null && position >= 0 && position < uris.length) {
+ listener.onGameSelected(uris[position]);
+ dismissAllowingStateLoss();
+ }
+ },
+ position -> {
+ // Long click - show game settings
+ if (position >= 0 && position < uris.length) {
+ showGameSettings(titles[position], uris[position]);
+ }
+ });
+ rv.setAdapter(adapter);
+
+ // Toolbar buttons
+ View btnHome = root.findViewById(R.id.btn_home);
+ if (btnHome != null) btnHome.setOnClickListener(v -> dismissAllowingStateLoss());
+ View btnDownload = root.findViewById(R.id.btn_download);
+ if (btnDownload != null) btnDownload.setOnClickListener(v -> startDownloadCovers());
+
+ AlertDialog dialog = new AlertDialog.Builder(requireContext())
+ .setView(root)
+ .create();
+ return dialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Dialog d = getDialog();
+ if (d != null) {
+ Window w = d.getWindow();
+ if (w != null) {
+ w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ w.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ // Hide status bar for true full-screen dialog
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ w.setDecorFitsSystemWindows(false);
+ WindowInsetsController controller = w.getInsetsController();
+ if (controller != null) {
+ controller.hide(WindowInsets.Type.statusBars());
+ controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ } else {
+ w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+ }
+ }
+
+ private int calculateSpanForWidth(int rvWidthPx, int itemDp, int spacingPx) {
+ float density = getResources().getDisplayMetrics().density;
+ int usable = Math.max(0, rvWidthPx);
+ int itemPx = (int) (itemDp * density);
+ // Include spacing in the packing calculation to avoid oscillation
+ // span = floor((usable + spacing) / (itemPx + spacing))
+ int span = (itemPx > 0) ? (int) Math.floor((usable + (double) spacingPx) / (itemPx + (double) spacingPx)) : 1;
+ return Math.max(2, Math.max(1, span));
+ }
+
+ private void preloadCovers(String[] urls) {
+ // Use Glide to warm cache
+ for (String url : urls) {
+ if (url == null) continue;
+ com.bumptech.glide.Glide.with(requireContext()).load(url).preload();
+ }
+ }
+
+ private void startDownloadCovers() {
+ Toast.makeText(requireContext(), "Downloading covers in background", Toast.LENGTH_SHORT).show();
+ new Thread(() -> {
+ // Try to refine serials/URLs by scanning disc contents first
+ SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ for (int i = 0; i < uris.length; i++) {
+ try {
+ // Prefer native serial extraction so CHDs work
+ String better = null;
+ try { better = NativeApp.getGameSerial(uris[i]); } catch (Throwable ignored) {}
+ if (better == null) better = extractSerialFromUri(uris[i]);
+ if (better != null && !better.equalsIgnoreCase(serialFromUrl(coverUrls[i]))) {
+ String serial = normalizeSerial(better);
+ coverUrls[i] = buildCoverUrlFromSerial(serial);
+ localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
+ editor.putString("serial:" + uris[i], serial);
+ }
+ } catch (Exception ignored) { }
+ }
+ editor.apply();
+
+ int total = coverUrls.length;
+ int ok = 0;
+ java.io.File dir = getCoversDir();
+ if (!dir.exists()) dir.mkdirs();
+ for (int i = 0; i < total; i++) {
+ String url = coverUrls[i];
+ String outPath = localPaths[i];
+ if (isFileValid(outPath)) { ok++; continue; }
+ try {
+ if (downloadToFile(url, outPath)) ok++;
+ } catch (Exception ignored) { }
+ }
+ final int downloaded = ok;
+ if (isAdded()) requireActivity().runOnUiThread(() -> {
+ Toast.makeText(requireContext(), "Covers ready: " + downloaded + "/" + total, Toast.LENGTH_SHORT).show();
+ // refresh adapter to prefer local files now
+ if (adapter != null) adapter.notifyDataSetChanged();
+ });
+ }).start();
+ }
+
+ private static String serialFromUrl(String url) {
+ if (url == null) return null;
+ int slash = url.lastIndexOf('/');
+ int dot = url.lastIndexOf('.');
+ if (slash >= 0 && dot > slash) return url.substring(slash + 1, dot);
+ return null;
+ }
+
+ private java.io.File getCoversDir() {
+ java.io.File base = requireContext().getExternalFilesDir("covers");
+ if (base == null) base = new java.io.File(requireContext().getFilesDir(), "covers");
+ return base;
+ }
+
+ private static boolean isFileValid(String path) {
+ if (path == null) return false;
+ java.io.File f = new java.io.File(path);
+ return f.exists() && f.length() > 0;
+ }
+
+ private static boolean downloadToFile(String urlStr, String outPath) throws Exception {
+ java.net.URL url = new java.net.URL(urlStr);
+ java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
+ conn.setConnectTimeout(10000);
+ conn.setReadTimeout(15000);
+ conn.setInstanceFollowRedirects(true);
+ conn.connect();
+ int code = conn.getResponseCode();
+ if (code != 200) { conn.disconnect(); return false; }
+ java.io.File outFile = new java.io.File(outPath);
+ java.io.File parent = outFile.getParentFile();
+ if (parent != null && !parent.exists()) parent.mkdirs();
+ java.io.InputStream in = conn.getInputStream();
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(outFile);
+ byte[] buf = new byte[8192];
+ int n;
+ while ((n = in.read(buf)) != -1) fos.write(buf, 0, n);
+ fos.flush();
+ fos.close();
+ in.close();
+ conn.disconnect();
+ return true;
+ }
+
+ private static String buildSerialFromUri(String gameUri) {
+ // Try to infer PS2 serial from file name: e.g., SLUS-20312 or SLPS_123.45 style
+ String last = Uri.parse(gameUri).getLastPathSegment();
+ if (last == null) last = "";
+ last = last.replace('_', '-');
+ // remove extension
+ int dot = last.lastIndexOf('.');
+ if (dot > 0) last = last.substring(0, dot);
+ String serial = null;
+ // Very simple heuristic: find token like XXXX-XXXXX
+ String upper = last.toUpperCase();
+ java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{4,5}-[0-9]{3,5})").matcher(upper);
+ if (m.find()) {
+ serial = m.group(1);
+ }
+ if (serial == null) {
+ serial = upper;
+ }
+ return serial;
+ }
+
+ private static String buildCoverUrlFromSerial(String serial) {
+ return "https://raw.githubusercontent.com/izzy2lost/ps2-covers/main/covers/3d/" + serial + ".png";
+ }
+
+ private String extractSerialFromUri(String gameUri) {
+ try {
+ java.io.InputStream in = requireContext().getContentResolver().openInputStream(Uri.parse(gameUri));
+ if (in == null) return null;
+ // Read first 8MB searching for SYSTEM.CNF contents, e.g., "BOOT2 = cdrom0:\\SLUS_203.12;1"
+ final int MAX_BYTES = 8 * 1024 * 1024;
+ final byte[] buf = new byte[64 * 1024];
+ int read;
+ int total = 0;
+ StringBuilder sb = new StringBuilder();
+ while ((read = in.read(buf)) != -1 && total < MAX_BYTES) {
+ total += read;
+ // append as ASCII
+ sb.append(new String(buf, 0, read));
+ // try to match as we go to avoid huge strings
+ String found = findSerialInString(sb);
+ if (found != null) { in.close(); return found; }
+ if (sb.length() > 512 * 1024) sb.delete(0, sb.length() - 128 * 1024); // keep window
+ }
+ in.close();
+ } catch (Exception ignored) { }
+ return null;
+ }
+
+ private static String findSerialInString(CharSequence cs) {
+ // Match common forms: SLUS_203.12, SLPM_650.51, SCES_123.45 etc.
+ java.util.regex.Matcher m = java.util.regex.Pattern
+ .compile("([A-Z]{4,5})[_-]([0-9]{3})\\.([0-9]{2})")
+ .matcher(cs);
+ if (m.find()) {
+ String prefix = m.group(1);
+ String part1 = m.group(2);
+ String part2 = m.group(3);
+ return prefix + "-" + part1 + part2; // SLUS-20312
+ }
+ return null;
+ }
+
+ private static String normalizeSerial(String serial) {
+ if (serial == null) return null;
+ String s = serial.toUpperCase().replace('_', '-');
+ // If form like XXXX-123.45 -> XXXX-12345
+ s = s.replaceAll("([A-Z]{4,5})-([0-9]{3})\\.([0-9]{2})", "$1-$2$3");
+ return s;
+ }
+
+ private void showGameSettings(String gameTitle, String gameUri) {
+ // Prefer native extraction so CHDs work
+ String gameSerial = null;
+ try { gameSerial = NativeApp.getGameSerial(gameUri); } catch (Throwable ignored) {}
+ if (gameSerial == null || gameSerial.isEmpty()) {
+ gameSerial = extractSerialFromUri(gameUri);
+ }
+ if (gameSerial == null || gameSerial.isEmpty()) {
+ gameSerial = buildSerialFromUri(gameUri);
+ }
+ gameSerial = normalizeSerial(gameSerial);
+
+ // CRC (native if available)
+ String gameCrc = null;
+ try { gameCrc = NativeApp.getGameCrc(gameUri); } catch (Throwable ignored) {}
+ if (gameCrc == null || gameCrc.isEmpty()) {
+ gameCrc = String.format("%08X", Math.abs(gameUri.hashCode()));
+ }
+
+ // Debug logging
+ android.util.Log.d("GameSettings", "Opening game settings for: " + gameTitle);
+ android.util.Log.d("GameSettings", "URI: " + gameUri);
+ android.util.Log.d("GameSettings", "Extracted Serial: " + gameSerial);
+ android.util.Log.d("GameSettings", "Generated CRC: " + gameCrc);
+
+ GameSettingsDialogFragment dialog = GameSettingsDialogFragment.newInstance(
+ gameTitle, gameUri, gameSerial, gameCrc);
+ dialog.show(getParentFragmentManager(), "game_settings");
+ }
+}
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDevice.java b/app/src/main/java/com/izzy2lost/psx2/HIDDevice.java
similarity index 94%
rename from app/src/main/java/kr/co/iefriends/pcsx2/HIDDevice.java
rename to app/src/main/java/com/izzy2lost/psx2/HIDDevice.java
index 33e7de0..a606502 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDevice.java
+++ b/app/src/main/java/com/izzy2lost/psx2/HIDDevice.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.hardware.usb.UsbDevice;
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceBLESteamController.java b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceBLESteamController.java
similarity index 99%
rename from app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceBLESteamController.java
rename to app/src/main/java/com/izzy2lost/psx2/HIDDeviceBLESteamController.java
index a08b86c..b73af38 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceBLESteamController.java
+++ b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceBLESteamController.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceManager.java b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceManager.java
similarity index 99%
rename from app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceManager.java
rename to app/src/main/java/com/izzy2lost/psx2/HIDDeviceManager.java
index b55692b..285feaf 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceManager.java
+++ b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceManager.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceUSB.java b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceUSB.java
similarity index 99%
rename from app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceUSB.java
rename to app/src/main/java/com/izzy2lost/psx2/HIDDeviceUSB.java
index f7804ac..bc3d3e4 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/HIDDeviceUSB.java
+++ b/app/src/main/java/com/izzy2lost/psx2/HIDDeviceUSB.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
diff --git a/app/src/main/java/com/izzy2lost/psx2/JoystickView.java b/app/src/main/java/com/izzy2lost/psx2/JoystickView.java
new file mode 100644
index 0000000..78c6dc3
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/JoystickView.java
@@ -0,0 +1,117 @@
+package com.izzy2lost.psx2;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import androidx.core.content.ContextCompat;
+
+public class JoystickView extends View {
+ public interface OnMoveListener {
+ void onMove(float nx, float ny, int action);
+ }
+
+ private final Paint basePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint ringPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint knobPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private float centerX, centerY, radius, knobX, knobY, knobRadius;
+ private boolean isDragging = false;
+ private OnMoveListener listener;
+
+ public JoystickView(Context ctx) { super(ctx); init(); }
+ public JoystickView(Context ctx, AttributeSet attrs) { super(ctx, attrs); init(); }
+ public JoystickView(Context ctx, AttributeSet attrs, int defStyle) { super(ctx, attrs, defStyle); init(); }
+
+ private void init() {
+ basePaint.setColor(0x22000000); // subtle fill
+ basePaint.setStyle(Paint.Style.FILL);
+ // Match Settings/Controls outline (brand primary blue)
+ int brandBlue = ContextCompat.getColor(getContext(), R.color.brand_primary);
+ ringPaint.setColor(brandBlue);
+ ringPaint.setStyle(Paint.Style.STROKE);
+ ringPaint.setStrokeWidth(dp(2));
+ // Knob uses the same brand blue
+ knobPaint.setColor(brandBlue);
+ knobPaint.setStyle(Paint.Style.FILL);
+ setClickable(true);
+ }
+
+ public void setOnMoveListener(OnMoveListener l) { this.listener = l; }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ centerX = w / 2f;
+ centerY = h / 2f;
+ // Make the visible base circle smaller relative to the view size
+ radius = Math.min(w, h) * 0.32f;
+ knobRadius = radius * 0.30f;
+ resetKnob();
+ }
+
+ private void resetKnob() {
+ knobX = centerX;
+ knobY = centerY;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ // Base circle
+ canvas.drawCircle(centerX, centerY, radius, basePaint);
+ // Outer thin ring
+ canvas.drawCircle(centerX, centerY, radius, ringPaint);
+ // Knob
+ canvas.drawCircle(knobX, knobY, knobRadius, knobPaint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ isDragging = true;
+ // fallthrough to move
+ case MotionEvent.ACTION_MOVE:
+ if (isDragging) {
+ float dx = event.getX() - centerX;
+ float dy = event.getY() - centerY;
+ // Clamp to circle
+ float dist = (float)Math.hypot(dx, dy);
+ if (dist > radius) {
+ float scale = radius / dist;
+ dx *= scale;
+ dy *= scale;
+ }
+ knobX = centerX + dx;
+ knobY = centerY + dy;
+ invalidate();
+ if (listener != null) {
+ // Normalize to [-1,1], invert Y so up is negative value (screen y grows down)
+ float nx = dx / radius;
+ float ny = dy / radius;
+ listener.onMove(clamp(nx), clamp(ny), MotionEvent.ACTION_MOVE);
+ }
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ isDragging = false;
+ resetKnob();
+ if (listener != null) listener.onMove(0f, 0f, MotionEvent.ACTION_UP);
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ private static float clamp(float v) { return Math.max(-1f, Math.min(1f, v)); }
+
+ private float dp(float d) {
+ return d * getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java
new file mode 100644
index 0000000..028f037
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java
@@ -0,0 +1,1280 @@
+package com.izzy2lost.psx2;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.net.Uri;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.view.ViewGroup;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.content.ClipData;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.util.TypedValue;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.content.ContextCompat;
+import androidx.activity.OnBackPressedCallback;
+
+import com.google.android.material.button.MaterialButton;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import android.provider.OpenableColumns;
+import org.json.JSONArray;
+import org.json.JSONException;
+import androidx.fragment.app.FragmentManager;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity implements GamesCoverDialogFragment.OnGameSelectedListener {
+ private String m_szGamefile = "";
+
+ private HIDDeviceManager mHIDDeviceManager;
+ private Thread mEmulationThread = null;
+ private boolean mHudVisible = false;
+
+
+ // Track joystick directional pressed state to avoid duplicate down events
+ private boolean joyUpPressed = false;
+ private boolean joyDownPressed = false;
+ private boolean joyLeftPressed = false;
+ private boolean joyRightPressed = false;
+
+ private boolean isThread() {
+ if (mEmulationThread != null) {
+ Thread.State _thread_state = mEmulationThread.getState();
+ return _thread_state == Thread.State.BLOCKED
+ || _thread_state == Thread.State.RUNNABLE
+ || _thread_state == Thread.State.TIMED_WAITING
+ || _thread_state == Thread.State.WAITING;
+ }
+ return false;
+ }
+
+ // Expose whether a game has been chosen (non-empty path)
+ public boolean hasSelectedGame() {
+ return !TextUtils.isEmpty(m_szGamefile);
+ }
+
+
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // Keep fullscreen on rotate and reflow constraints without recreating
+ hideStatusBar();
+ applyConstraintsForOrientation(newConfig.orientation);
+ }
+
+ private void applyConstraintsForOrientation(int orientation) {
+ // Views present in both layouts
+ View quick = findViewById(R.id.ll_quick_actions);
+ View btnSettings = findViewById(R.id.btn_settings);
+ View btnControls = findViewById(R.id.btn_toggle_controls);
+ View llJoy = findViewById(R.id.ll_pad_joy);
+ View llDpad = findViewById(R.id.ll_pad_dpad);
+ View llRight = findViewById(R.id.ll_pad_right_buttons);
+ View llSelectStart = findViewById(R.id.ll_pad_select_start);
+
+ // Use helper dp()
+
+ if (quick != null && btnSettings != null && btnControls != null) {
+ ConstraintLayout.LayoutParams lp = safeCLP(quick);
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ // Between settings and controls, top-aligned
+ lp.width = 0; // chain between start/end
+ lp.topToTop = btnSettings.getId();
+ lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
+ lp.startToEnd = btnSettings.getId();
+ lp.endToStart = btnControls.getId();
+ lp.startToStart = ConstraintLayout.LayoutParams.UNSET;
+ lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+ lp.topMargin = dp(0);
+ lp.horizontalChainStyle = ConstraintLayout.LayoutParams.CHAIN_PACKED;
+ } else {
+ // Centered under settings
+ lp.width = ConstraintLayout.LayoutParams.WRAP_CONTENT;
+ lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
+ lp.topToBottom = btnSettings.getId();
+ lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.startToEnd = ConstraintLayout.LayoutParams.UNSET;
+ lp.endToStart = ConstraintLayout.LayoutParams.UNSET;
+ lp.topMargin = dp(16);
+ lp.horizontalChainStyle = ConstraintLayout.LayoutParams.CHAIN_SPREAD;
+ }
+ quick.setLayoutParams(lp);
+ }
+
+ if (llJoy != null) {
+ ConstraintLayout.LayoutParams lp = safeCLP(llJoy);
+ lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+ lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
+ lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
+ int m = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? 6 : 6;
+ lp.setMargins(dp(m), dp(m), dp(m), dp(m));
+ llJoy.setLayoutParams(lp);
+ // Nudge joystick further left in both orientations to avoid Select overlap
+ llJoy.setTranslationX(-dp(28));
+ }
+
+ if (llDpad != null && llJoy != null) {
+ ConstraintLayout.LayoutParams lp = safeCLP(llDpad);
+ // Above-left of joystick for both, with slight spacing
+ lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+ lp.bottomToTop = llJoy.getId();
+ lp.bottomToBottom = ConstraintLayout.LayoutParams.UNSET;
+ lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
+ lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
+ lp.setMargins(dp(0), dp(0), dp(0), dp(orientation == Configuration.ORIENTATION_LANDSCAPE ? 2 : 0));
+ llDpad.setLayoutParams(lp);
+ }
+
+ if (llRight != null) {
+ ConstraintLayout.LayoutParams lp = safeCLP(llRight);
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.bottomToTop = ConstraintLayout.LayoutParams.UNSET;
+ lp.setMargins(dp(12), dp(12), dp(12), dp(12));
+ } else {
+ // Above Select/Start, aligned to end
+ if (llSelectStart != null) {
+ lp.bottomToTop = llSelectStart.getId();
+ } else {
+ lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ }
+ lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.bottomToBottom = (llSelectStart == null) ? ConstraintLayout.LayoutParams.PARENT_ID : ConstraintLayout.LayoutParams.UNSET;
+ lp.setMargins(0,0,0,dp(8));
+ }
+ llRight.setLayoutParams(lp);
+ }
+
+ if (llSelectStart != null) {
+ ConstraintLayout.LayoutParams lp = safeCLP(llSelectStart);
+ lp.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
+ lp.setMargins(0,0,0,dp(orientation == Configuration.ORIENTATION_LANDSCAPE ? 8 : 0));
+ llSelectStart.setLayoutParams(lp);
+ }
+ }
+
+ private ConstraintLayout.LayoutParams safeCLP(View v) {
+ ViewGroup.LayoutParams p = v.getLayoutParams();
+ if (p instanceof ConstraintLayout.LayoutParams) return (ConstraintLayout.LayoutParams) p;
+ ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(p);
+ v.setLayoutParams(lp);
+ return lp;
+ }
+
+ private int dp(int d) {
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, d, getResources().getDisplayMetrics());
+ }
+
+ private void hideStatusBar() {
+ Window w = getWindow();
+ if (w == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ WindowInsetsController controller = w.getInsetsController();
+ if (controller != null) {
+ controller.hide(WindowInsets.Type.statusBars());
+ controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ } else {
+ w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ private void pickGamesFolder() {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ startActivityResultGamesFolderPick.launch(intent);
+ }
+
+ private void showGamesListOrReselect(Uri treeUri) {
+ // Re-scan quickly each time to keep list fresh
+ String[] names;
+ String[] uris;
+ try {
+ GameList list = scanGamesFromTreeUri(treeUri);
+ names = list.names;
+ uris = list.uris;
+ } catch (Exception e) {
+ names = new String[0];
+ uris = new String[0];
+ }
+ // Make effectively-final copies for use in lambda
+ final String[] namesFinal = names;
+ final String[] urisFinal = uris;
+
+ if (namesFinal.length == 0) {
+ new AlertDialog.Builder(this)
+ .setTitle("GAMES")
+ .setMessage("No games found. Pick a folder?")
+ .setNegativeButton("Cancel", null)
+ .setPositiveButton("Pick Folder", (d,w) -> pickGamesFolder())
+ .show();
+ return;
+ }
+ // Show covers grid dialog fragment
+ GamesCoverDialogFragment frag = GamesCoverDialogFragment.newInstance(namesFinal, urisFinal);
+ FragmentManager fm = getSupportFragmentManager();
+ frag.show(fm, "covers_dialog");
+ }
+
+ @Override
+ public void onGameSelected(String gameUri) {
+ if (!TextUtils.isEmpty(gameUri)) {
+ // Avoid any pre-VM native calls here; just set the game and launch.
+ m_szGamefile = gameUri;
+ restartEmuThread();
+ }
+ }
+
+ private static final String[] GAME_EXTS = new String[]{
+ ".iso", ".bin", ".img", ".mdf", ".nrg", ".chd"
+ };
+
+ private static boolean hasGameExt(String name) {
+ if (TextUtils.isEmpty(name)) return false;
+ String lower = name.toLowerCase();
+ for (String ext : GAME_EXTS) {
+ if (lower.endsWith(ext)) return true;
+ }
+ return false;
+ }
+
+ private static class GameList {
+ final String[] names;
+ final String[] uris;
+ GameList(String[] n, String[] u) { names = n; uris = u; }
+ }
+
+ private GameList scanGamesFromTreeUri(Uri treeUri) {
+ DocumentFile dir = DocumentFile.fromTreeUri(this, treeUri);
+ if (dir == null || !dir.isDirectory()) return new GameList(new String[0], new String[0]);
+ java.util.ArrayList nameList = new java.util.ArrayList<>();
+ java.util.ArrayList uriList = new java.util.ArrayList<>();
+ scanGamesRecursive(dir, nameList, uriList);
+
+ // Persist folder and latest list
+ SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
+ JSONArray arr = new JSONArray();
+ for (int i = 0; i < nameList.size(); i++) {
+ JSONArray pair = new JSONArray();
+ try {
+ pair.put(nameList.get(i));
+ pair.put(uriList.get(i));
+ } catch (Exception ignored) {}
+ arr.put(pair);
+ }
+ prefs.edit()
+ .putString("games_folder_uri", treeUri.toString())
+ .putString("games_list_json", arr.toString())
+ .apply();
+
+ return new GameList(nameList.toArray(new String[0]), uriList.toArray(new String[0]));
+ }
+
+ private void scanGamesRecursive(DocumentFile dir, java.util.List names, java.util.List uris) {
+ DocumentFile[] children = dir.listFiles();
+ if (children == null) return;
+ for (DocumentFile child : children) {
+ if (child == null) continue;
+ if (child.isDirectory()) {
+ scanGamesRecursive(child, names, uris);
+ } else if (child.isFile()) {
+ String name = child.getName();
+ if (hasGameExt(name)) {
+ names.add(name);
+ uris.add(child.getUri().toString());
+ }
+ }
+ }
+ }
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ hideStatusBar();
+
+ // Setup back button handler
+ setupBackPressedHandler();
+
+ // Default resources
+ copyAssetAll(getApplicationContext(), "bios");
+ copyAssetAll(getApplicationContext(), "resources");
+
+ Initialize();
+
+ makeButtonTouch();
+
+ setSurfaceView(new SDLSurface(this));
+
+ // Improve button outline contrast across all MaterialButtons
+ tintAllMaterialButtonOutlines();
+
+ // Apply saved graphics settings (renderer, scaling, aspect)
+ applySavedSettings();
+
+ // Apply orientation-specific constraints once at startup
+ int currentOrientation = getResources().getConfiguration().orientation;
+ applyConstraintsForOrientation(currentOrientation);
+
+
+ }
+
+ private void setupBackPressedHandler() {
+ OnBackPressedCallback callback = new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ showExitDialog();
+ }
+ };
+ getOnBackPressedDispatcher().addCallback(this, callback);
+ }
+
+ private void makeButtonTouch() {
+ MaterialButton btn_file = findViewById(R.id.btn_file);
+ if(btn_file != null) {
+ // Tap: show games list (from persisted folder). If none, prompt to pick folder.
+ btn_file.setOnClickListener(v -> {
+ SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
+ String folderUri = prefs.getString("games_folder_uri", null);
+ if (TextUtils.isEmpty(folderUri)) {
+ pickGamesFolder();
+ return;
+ }
+ showGamesListOrReselect(Uri.parse(folderUri));
+ });
+ // Long-press: reselect games folder
+ btn_file.setOnLongClickListener(v -> {
+ pickGamesFolder();
+ return true;
+ });
+ }
+
+ // Combined saves dialog
+ MaterialButton btn_saves = findViewById(R.id.btn_saves);
+ if(btn_saves != null) {
+ btn_saves.setOnClickListener(v -> {
+ SavesDialogFragment dialog = new SavesDialogFragment();
+ dialog.show(getSupportFragmentManager(), "saves_dialog");
+ });
+ }
+
+ // BIOS picker
+ MaterialButton btn_bios = findViewById(R.id.btn_bios);
+ if (btn_bios != null) {
+ // Tap: pick multiple BIOS files
+ btn_bios.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+ intent.setType("*/*");
+ startActivityResultBiosPick.launch(intent);
+ });
+ // Long-press: pick a BIOS folder
+ btn_bios.setOnLongClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ startActivityResultBiosFolderPick.launch(intent);
+ return true;
+ });
+ }
+
+ // Settings button opens dialog
+ MaterialButton btn_settings = findViewById(R.id.btn_settings);
+ if (btn_settings != null) {
+ btn_settings.setOnClickListener(v -> {
+ FragmentManager fm = getSupportFragmentManager();
+ SettingsDialogFragment dialog = new SettingsDialogFragment();
+ dialog.show(fm, "settings_dialog");
+ });
+ }
+
+ // Toggle on-screen controls visibility
+ MaterialButton btnToggleControls = findViewById(R.id.btn_toggle_controls);
+ if (btnToggleControls != null) {
+ btnToggleControls.setOnClickListener(v -> {
+ View llDpad = findViewById(R.id.ll_pad_dpad);
+ View llRight = findViewById(R.id.ll_pad_right_buttons);
+ View llSelectStart = findViewById(R.id.ll_pad_select_start);
+ View llJoy = findViewById(R.id.ll_pad_joy);
+
+ boolean currentlyVisible = (llDpad != null && llDpad.getVisibility() == View.VISIBLE);
+ setControlsVisible(!currentlyVisible);
+ });
+ }
+
+ // HUD toggle moved to Settings (Developer section)
+
+ // Hide UI button
+ MaterialButton btn_hide_ui = findViewById(R.id.btn_hide_ui);
+ if(btn_hide_ui != null) {
+ btn_hide_ui.setOnClickListener(v -> {
+ toggleAllUIVisibility();
+ });
+ }
+
+ // Small unhide button (appears when all UI is hidden)
+ MaterialButton btn_unhide_ui = findViewById(R.id.btn_unhide_ui);
+ if(btn_unhide_ui != null) {
+ btn_unhide_ui.setOnClickListener(v -> {
+ toggleAllUIVisibility(); // Show UI again
+ });
+ }
+
+ //////
+ // RENDERER
+
+ MaterialButton btn_ogl = findViewById(R.id.btn_ogl);
+ if(btn_ogl != null) {
+ btn_ogl.setOnClickListener(v -> {
+ NativeApp.renderGpu(12);
+ });
+ }
+ MaterialButton btn_vulkan = findViewById(R.id.btn_vulkan);
+ if(btn_vulkan != null) {
+ btn_vulkan.setOnClickListener(v -> {
+ NativeApp.renderGpu(14);
+ });
+ }
+ MaterialButton btn_sw = findViewById(R.id.btn_sw);
+ if(btn_sw != null) {
+ btn_sw.setOnClickListener(v -> {
+ NativeApp.renderGpu(13);
+ });
+ }
+
+ //////
+ // PAD
+
+ MaterialButton btn_pad_select = findViewById(R.id.btn_pad_select);
+ if(btn_pad_select != null) {
+ btn_pad_select.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_SELECT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_start = findViewById(R.id.btn_pad_start);
+ if(btn_pad_start != null) {
+ btn_pad_start.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_START);
+ return true;
+ });
+ }
+
+ MaterialButton btn_pad_a = findViewById(R.id.btn_pad_a);
+ if(btn_pad_a != null) {
+ btn_pad_a.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_A);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_b = findViewById(R.id.btn_pad_b);
+ if(btn_pad_b != null) {
+ btn_pad_b.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_B);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_x = findViewById(R.id.btn_pad_x);
+ if(btn_pad_x != null) {
+ btn_pad_x.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_X);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_y = findViewById(R.id.btn_pad_y);
+ if(btn_pad_y != null) {
+ btn_pad_y.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_Y);
+ return true;
+ });
+ }
+
+ ////
+
+ MaterialButton btn_pad_l1 = findViewById(R.id.btn_pad_l1);
+ if(btn_pad_l1 != null) {
+ btn_pad_l1.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L1);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_r1 = findViewById(R.id.btn_pad_r1);
+ if(btn_pad_r1 != null) {
+ btn_pad_r1.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R1);
+ return true;
+ });
+ }
+
+ MaterialButton btn_pad_l2 = findViewById(R.id.btn_pad_l2);
+ if(btn_pad_l2 != null) {
+ btn_pad_l2.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L2);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_r2 = findViewById(R.id.btn_pad_r2);
+ if(btn_pad_r2 != null) {
+ btn_pad_r2.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R2);
+ return true;
+ });
+ }
+
+ MaterialButton btn_pad_l3 = findViewById(R.id.btn_pad_l3);
+ if(btn_pad_l3 != null) {
+ btn_pad_l3.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBL);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_r3 = findViewById(R.id.btn_pad_r3);
+ if(btn_pad_r3 != null) {
+ btn_pad_r3.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBR);
+ return true;
+ });
+ }
+
+ ////
+
+ final int PAD_L_UP = 110;
+ final int PAD_L_RIGHT = 111;
+ final int PAD_L_DOWN = 112;
+ final int PAD_L_LEFT = 113;
+
+ final int PAD_R_UP = 120;
+ final int PAD_R_RIGHT = 121;
+ final int PAD_R_DOWN = 122;
+ final int PAD_R_LEFT = 123;
+
+ MaterialButton btn_pad_joy_lt = findViewById(R.id.btn_pad_joy_lt);
+ if(btn_pad_joy_lt != null) {
+ btn_pad_joy_lt.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_UP);
+ sendKeyAction(v, event.getAction(), PAD_L_LEFT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_t = findViewById(R.id.btn_pad_joy_t);
+ if(btn_pad_joy_t != null) {
+ btn_pad_joy_t.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_UP);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_rt = findViewById(R.id.btn_pad_joy_rt);
+ if(btn_pad_joy_rt != null) {
+ btn_pad_joy_rt.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_UP);
+ sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_l = findViewById(R.id.btn_pad_joy_l);
+ if(btn_pad_joy_l != null) {
+ btn_pad_joy_l.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_LEFT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_r = findViewById(R.id.btn_pad_joy_r);
+ if(btn_pad_joy_r != null) {
+ btn_pad_joy_r.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_lb = findViewById(R.id.btn_pad_joy_lb);
+ if(btn_pad_joy_lb != null) {
+ btn_pad_joy_lb.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_LEFT);
+ sendKeyAction(v, event.getAction(), PAD_L_DOWN);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_b = findViewById(R.id.btn_pad_joy_b);
+ if(btn_pad_joy_b != null) {
+ btn_pad_joy_b.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_DOWN);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_joy_rb = findViewById(R.id.btn_pad_joy_rb);
+ if(btn_pad_joy_rb != null) {
+ btn_pad_joy_rb.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
+ sendKeyAction(v, event.getAction(), PAD_L_DOWN);
+ return true;
+ });
+ }
+
+ // Draggable JoystickView (portrait/landscape layouts)
+ View joystick = findViewById(R.id.joystick_view);
+ if (joystick instanceof JoystickView) {
+ JoystickView jv = (JoystickView) joystick;
+ jv.setOnMoveListener((nx, ny, action) -> {
+ // Thresholds
+ final float T = 0.3f;
+ boolean up = ny < -T;
+ boolean down = ny > T;
+ boolean left = nx < -T;
+ boolean right = nx > T;
+
+ // Issue key down/up transitions only when state changes
+ if (up != joyUpPressed) {
+ sendKeyAction(jv, up ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_UP);
+ joyUpPressed = up;
+ }
+ if (down != joyDownPressed) {
+ sendKeyAction(jv, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_DOWN);
+ joyDownPressed = down;
+ }
+ if (left != joyLeftPressed) {
+ sendKeyAction(jv, left ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_LEFT);
+ joyLeftPressed = left;
+ }
+ if (right != joyRightPressed) {
+ sendKeyAction(jv, right ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_L_RIGHT);
+ joyRightPressed = right;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ // Ensure all released
+ if (joyUpPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_UP);
+ if (joyDownPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_DOWN);
+ if (joyLeftPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_LEFT);
+ if (joyRightPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_L_RIGHT);
+ joyUpPressed = joyDownPressed = joyLeftPressed = joyRightPressed = false;
+ }
+ });
+ }
+
+ ////
+
+ MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top);
+ if(btn_pad_dir_top != null) {
+ btn_pad_dir_top.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_UP);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_dir_bottom = findViewById(R.id.btn_pad_dir_bottom);
+ if(btn_pad_dir_bottom != null) {
+ btn_pad_dir_bottom.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_DOWN);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_dir_left = findViewById(R.id.btn_pad_dir_left);
+ if(btn_pad_dir_left != null) {
+ btn_pad_dir_left.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_LEFT);
+ return true;
+ });
+ }
+ MaterialButton btn_pad_dir_right = findViewById(R.id.btn_pad_dir_right);
+ if(btn_pad_dir_right != null) {
+ btn_pad_dir_right.setOnTouchListener((v, event) -> {
+ sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_RIGHT);
+ return true;
+ });
+ }
+ }
+
+ private boolean allUIHidden = false;
+
+ private void setControlsVisible(boolean visible) {
+ int vis = visible ? View.VISIBLE : View.GONE;
+ View llDpad = findViewById(R.id.ll_pad_dpad);
+ View llRight = findViewById(R.id.ll_pad_right_buttons);
+ View llSelectStart = findViewById(R.id.ll_pad_select_start);
+ View llJoy = findViewById(R.id.ll_pad_joy);
+
+ if (llDpad != null) llDpad.setVisibility(vis);
+ if (llRight != null) llRight.setVisibility(vis);
+ if (llSelectStart != null) llSelectStart.setVisibility(vis);
+ if (llJoy != null) llJoy.setVisibility(vis);
+ }
+
+ private void toggleAllUIVisibility() {
+ allUIHidden = !allUIHidden;
+ int vis = allUIHidden ? View.GONE : View.VISIBLE;
+
+ // Hide/show all UI elements
+ View btnSettings = findViewById(R.id.btn_settings);
+ View btnToggleControls = findViewById(R.id.btn_toggle_controls);
+ View btnFile = findViewById(R.id.btn_file);
+ View btnBios = findViewById(R.id.btn_bios);
+ View btnSaves = findViewById(R.id.btn_saves);
+ View btnHideUI = findViewById(R.id.btn_hide_ui);
+
+ if (btnSettings != null) btnSettings.setVisibility(vis);
+ if (btnToggleControls != null) btnToggleControls.setVisibility(vis);
+ if (btnFile != null) btnFile.setVisibility(vis);
+ if (btnBios != null) btnBios.setVisibility(vis);
+ if (btnSaves != null) btnSaves.setVisibility(vis);
+ if (btnHideUI != null) btnHideUI.setVisibility(vis);
+
+ // Hide/show on-screen controls
+ if (!allUIHidden) {
+ // When showing UI again, restore controls to their previous state
+ setControlsVisible(true); // You might want to remember the previous state
+ } else {
+ // When hiding all UI, also hide controls
+ setControlsVisible(false);
+ }
+
+ // Show a small unhide button when UI is hidden
+ View unhideButton = findViewById(R.id.btn_unhide_ui);
+ if (unhideButton != null) {
+ unhideButton.setVisibility(allUIHidden ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void tintAllMaterialButtonOutlines() {
+ // Brand strokes
+ final ColorStateList strokeDefault = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_outline));
+ final ColorStateList strokePrimary = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_primary));
+ final ColorStateList strokeSecondary = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_secondary));
+
+ View root = findViewById(android.R.id.content);
+ if (root instanceof ViewGroup) {
+ traverseAndTintButtons((ViewGroup) root, strokeDefault, strokePrimary, strokeSecondary);
+ }
+ }
+
+ private void traverseAndTintButtons(ViewGroup group, ColorStateList strokeDefault, ColorStateList strokePrimary, ColorStateList strokeSecondary) {
+ for (int i = 0; i < group.getChildCount(); i++) {
+ View child = group.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ traverseAndTintButtons((ViewGroup) child, strokeDefault, strokePrimary, strokeSecondary);
+ }
+ if (child instanceof MaterialButton) {
+ MaterialButton mb = (MaterialButton) child;
+ // Ensure ripple matches brand globally
+ mb.setRippleColor(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_ripple)));
+ int id = mb.getId();
+ if (id == R.id.btn_settings || id == R.id.btn_toggle_controls) {
+ mb.setStrokeColor(strokePrimary);
+ } else if (id == R.id.btn_file || id == R.id.btn_bios || id == R.id.btn_saves || id == R.id.btn_hide_ui) {
+ mb.setStrokeColor(strokeSecondary);
+ } else if (id == R.id.btn_pad_y) { // Triangle = green
+ final int base = ContextCompat.getColor(this, R.color.ps2_triangle_green);
+ ColorStateList stateful = pressedColorStateList(base);
+ mb.setStrokeColor(stateful);
+ mb.setTextColor(stateful);
+ } else if (id == R.id.btn_pad_b) { // Circle = red
+ final int base = ContextCompat.getColor(this, R.color.ps2_circle_red);
+ ColorStateList stateful = pressedColorStateList(base);
+ mb.setStrokeColor(stateful);
+ mb.setTextColor(stateful);
+ } else if (id == R.id.btn_pad_a) { // Cross = blue
+ final int base = ContextCompat.getColor(this, R.color.ps2_cross_blue);
+ ColorStateList stateful = pressedColorStateList(base);
+ mb.setStrokeColor(stateful);
+ mb.setTextColor(stateful);
+ } else if (id == R.id.btn_pad_x) { // Square = pink
+ final int base = ContextCompat.getColor(this, R.color.ps2_square_pink);
+ ColorStateList stateful = pressedColorStateList(base);
+ mb.setStrokeColor(stateful);
+ mb.setTextColor(stateful);
+ } else if (id == R.id.btn_pad_dir_top || id == R.id.btn_pad_dir_left || id == R.id.btn_pad_dir_right || id == R.id.btn_pad_dir_bottom) {
+ // D-pad arrow icon tint to brand accent
+ ColorStateList iconTint = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.brand_accent));
+ mb.setIconTint(iconTint);
+ } else {
+ mb.setStrokeColor(strokeDefault);
+ }
+ mb.setStrokeWidth(1);
+ }
+ }
+ }
+
+ private ColorStateList pressedColorStateList(int base) {
+ int pressed = darkenColor(base, 0.85f); // 15% darker when pressed
+ int[][] states = new int[][]{
+ new int[]{android.R.attr.state_pressed},
+ new int[]{}
+ };
+ int[] colors = new int[]{
+ pressed,
+ base
+ };
+ return new ColorStateList(states, colors);
+ }
+
+ private int darkenColor(int color, float factor) {
+ int a = (color >> 24) & 0xFF;
+ int r = (color >> 16) & 0xFF;
+ int g = (color >> 8) & 0xFF;
+ int b = color & 0xFF;
+ r = Math.max(0, Math.min(255, (int)(r * factor)));
+ g = Math.max(0, Math.min(255, (int)(g * factor)));
+ b = Math.max(0, Math.min(255, (int)(b * factor)));
+ return (a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ private void applySavedSettings() {
+ SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
+ // Renderer: 12=OpenGL, 13=Software, 14=Vulkan
+ int renderer = prefs.getInt("renderer", 14);
+ NativeApp.renderGpu(renderer);
+
+ // Resolution scale multiplier (float), default 1.0
+ float scale = prefs.getFloat("upscale_multiplier", 1.0f);
+ NativeApp.renderUpscalemultiplier(scale);
+
+ // Aspect ratio: 0=Stretch, 1=Auto 4:3/3:2, 2=4:3, 3=16:9, 4=10:7
+ int aspectRatio = prefs.getInt("aspect_ratio", 1); // Default to Auto 4:3/3:2
+ NativeApp.setAspectRatio(aspectRatio);
+
+ // Widescreen patches
+ boolean widescreenPatches = prefs.getBoolean("widescreen_patches", false);
+ NativeApp.setWidescreenPatches(widescreenPatches);
+
+ // No interlacing patches
+ boolean noInterlacingPatches = prefs.getBoolean("no_interlacing_patches", false);
+ NativeApp.setNoInterlacingPatches(noInterlacingPatches);
+
+ // HUD visibility
+ boolean hudVisible = prefs.getBoolean("hud_visible", false);
+ NativeApp.setHudVisible(hudVisible);
+ }
+
+ public final ActivityResultLauncher startActivityResultLocalFilePlay = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if(result.getResultCode() == Activity.RESULT_OK) {
+ try {
+ Intent _intent = result.getData();
+ if(_intent != null) {
+ m_szGamefile = _intent.getDataString();
+ if(!TextUtils.isEmpty(m_szGamefile)) {
+ restartEmuThread();
+ }
+ }
+ }
+ catch (Exception ignored) {}
+ }
+ }
+ );
+
+ public final ActivityResultLauncher startActivityResultBiosPick = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ try {
+ Intent data = result.getData();
+ if (data != null) {
+ File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
+ if (!biosDir.exists()) biosDir.mkdirs();
+
+ ClipData clipData = data.getClipData();
+ if (clipData != null && clipData.getItemCount() > 0) {
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ Uri uri = clipData.getItemAt(i).getUri();
+ importSingleBiosUri(uri, biosDir);
+ }
+ } else {
+ Uri uri = data.getData();
+ if (uri != null) {
+ importSingleBiosUri(uri, biosDir);
+ }
+ }
+
+ }
+ } catch (Exception ignored) {}
+ }
+ });
+
+ public final ActivityResultLauncher startActivityResultBiosFolderPick = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ try {
+ Intent data = result.getData();
+ if (data != null) {
+ Uri treeUri = data.getData();
+ if (treeUri != null) {
+ // Persist read permission for future imports, optional
+ final int takeFlags = (data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+ getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
+
+ DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
+ if (pickedDir != null && pickedDir.isDirectory()) {
+ File biosDir = new File(getApplicationContext().getExternalFilesDir(null), "bios");
+ if (!biosDir.exists()) biosDir.mkdirs();
+ copyDocumentTreeToDirectory(pickedDir, biosDir);
+ }
+ }
+ }
+ } catch (Exception ignored) {}
+ }
+ });
+
+ public final ActivityResultLauncher startActivityResultGamesFolderPick = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> {
+ if (result.getResultCode() == Activity.RESULT_OK) {
+ try {
+ Intent data = result.getData();
+ if (data != null) {
+ Uri treeUri = data.getData();
+ if (treeUri != null) {
+ final int takeFlags = (data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+ try {
+ getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
+ } catch (SecurityException ignored) {}
+ // Save folder and show games
+ getSharedPreferences("app_prefs", MODE_PRIVATE)
+ .edit()
+ .putString("games_folder_uri", treeUri.toString())
+ .apply();
+ showGamesListOrReselect(treeUri);
+ }
+ }
+ } catch (Exception ignored) {}
+ }
+ });
+
+ private void importSingleBiosUri(Uri uri, File biosDir) {
+ if (uri == null) return;
+ String displayName = getDisplayNameFromUri(this, uri);
+ if (TextUtils.isEmpty(displayName)) displayName = "bios.bin";
+ File outFile = new File(biosDir, displayName);
+ copyUriToFile(this, uri, outFile);
+ }
+
+ private void copyDocumentTreeToDirectory(DocumentFile dir, File outDir) {
+ if (dir == null || !dir.isDirectory()) return;
+ DocumentFile[] children = dir.listFiles();
+ if (children == null) return;
+ for (DocumentFile child : children) {
+ if (child == null) continue;
+ if (child.isDirectory()) {
+ File sub = new File(outDir, child.getName() != null ? child.getName() : "folder");
+ if (!sub.exists()) sub.mkdirs();
+ copyDocumentTreeToDirectory(child, sub);
+ } else if (child.isFile()) {
+ String name = child.getName();
+ if (TextUtils.isEmpty(name)) name = "bios.bin";
+ File dest = new File(outDir, name);
+ copyUriToFile(this, child.getUri(), dest);
+ }
+ }
+ }
+
+ private static String getDisplayNameFromUri(Context context, Uri uri) {
+ String name = null;
+ Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+ if (cursor != null) {
+ try {
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (nameIndex >= 0 && cursor.moveToFirst()) {
+ name = cursor.getString(nameIndex);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ if (TextUtils.isEmpty(name)) {
+ name = uri.getLastPathSegment();
+ }
+ return name;
+ }
+
+ private static boolean copyUriToFile(Context context, Uri uri, File destFile) {
+ InputStream is = null;
+ FileOutputStream os = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ if (is == null) return false;
+ os = new FileOutputStream(destFile);
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = is.read(buffer)) != -1) {
+ os.write(buffer, 0, read);
+ }
+ os.flush();
+ return true;
+ } catch (Exception e) {
+ return false;
+ } finally {
+ try { if (is != null) is.close(); } catch (Exception ignored) {}
+ try { if (os != null) os.close(); } catch (Exception ignored) {}
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ NativeApp.pause();
+ super.onPause();
+ ////
+ if (mHIDDeviceManager != null) {
+ mHIDDeviceManager.setFrozen(true);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ NativeApp.resume();
+ super.onResume();
+ ////
+ if (mHIDDeviceManager != null) {
+ mHIDDeviceManager.setFrozen(false);
+ }
+ // Re-assert full screen when returning to the activity
+ hideStatusBar();
+ }
+
+ @Override
+ protected void onDestroy() {
+ NativeApp.shutdown();
+ super.onDestroy();
+ ////
+ if (mHIDDeviceManager != null) {
+ HIDDeviceManager.release(mHIDDeviceManager);
+ mHIDDeviceManager = null;
+ }
+ ////
+ if (mEmulationThread != null) {
+ try {
+ mEmulationThread.join();
+ mEmulationThread = null;
+ }
+ catch (InterruptedException ignored) {}
+ }
+
+ int appPid = android.os.Process.myPid();
+ android.os.Process.killProcess(appPid);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+
+ public void Initialize() {
+ NativeApp.initializeOnce(getApplicationContext());
+
+ // Set up JNI
+ SDLControllerManager.nativeSetupJNI();
+
+ // Initialize state
+ SDLControllerManager.initialize();
+
+ // Load and apply saved settings
+ SettingsDialogFragment.loadAndApplySettings(this);
+
+ mHIDDeviceManager = HIDDeviceManager.acquire(this);
+ }
+
+ private void setSurfaceView(Object p_value) {
+ FrameLayout fl_board = findViewById(R.id.fl_board);
+ if(fl_board != null) {
+ if(fl_board.getChildCount() > 0) {
+ fl_board.removeAllViews();
+ }
+ ////
+ if(p_value instanceof SDLSurface) {
+ fl_board.addView((SDLSurface)p_value);
+ }
+ }
+ }
+
+ public void startEmuThread() {
+ if(!isThread()) {
+ mEmulationThread = new Thread(() -> NativeApp.runVMThread(m_szGamefile));
+ mEmulationThread.start();
+ }
+ }
+
+ private void restartEmuThread() {
+ NativeApp.shutdown();
+ if (mEmulationThread != null) {
+ try {
+ mEmulationThread.join();
+ mEmulationThread = null;
+ }
+ catch (InterruptedException ignored) {}
+ }
+ ////
+ startEmuThread();
+ }
+
+ // Public API for UI components to reboot the emulator
+ public void rebootEmu() {
+ if (!TextUtils.isEmpty(m_szGamefile)) {
+ restartEmuThread();
+ } else {
+ // No game loaded; just shutdown for safety
+ NativeApp.shutdown();
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ if (SDLControllerManager.isDeviceSDLJoystick(event.getDeviceId())) {
+ SDLControllerManager.handleJoystickMotionEvent(event);
+ return true;
+ }
+ return super.onGenericMotionEvent(event);
+ }
+
+ @Override
+ public boolean onKeyDown(int p_keyCode, KeyEvent p_event) {
+ if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ if (p_event.getRepeatCount() == 0) {
+ SDLControllerManager.onNativePadDown(p_event.getDeviceId(), p_keyCode);
+ return true;
+ }
+ }
+ else {
+ if (p_keyCode == KeyEvent.KEYCODE_BACK) {
+ showExitDialog();
+ return true;
+ }
+ }
+ return super.onKeyDown(p_keyCode, p_event);
+ }
+
+ @Override
+ public boolean onKeyUp(int p_keyCode, KeyEvent p_event) {
+ if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
+ if (p_event.getRepeatCount() == 0) {
+ SDLControllerManager.onNativePadUp(p_event.getDeviceId(), p_keyCode);
+ return true;
+ }
+ }
+ return super.onKeyUp(p_keyCode, p_event);
+ }
+
+ public static void sendKeyAction(View p_view, int p_action, int p_keycode) {
+ if(p_action == MotionEvent.ACTION_DOWN) {
+ p_view.setPressed(true);
+ int pad_force = 0;
+ if(p_keycode >= 110) {
+ float _abs = 90; // Joystic test value
+ _abs = Math.min(_abs, 100);
+ pad_force = (int) (_abs * 32766.0f / 100);
+ }
+ NativeApp.setPadButton(p_keycode, pad_force, true);
+ } else if(p_action == MotionEvent.ACTION_UP || p_action == MotionEvent.ACTION_CANCEL) {
+ p_view.setPressed(false);
+ NativeApp.setPadButton(p_keycode, 0, false);
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Asset copy helpers used on first launch to seed default resources
+ private void copyAssetAll(Context context, String srcPath) {
+ AssetManager assetMgr = context.getAssets();
+ try {
+ String[] assets = assetMgr.list(srcPath);
+ String destPath = context.getExternalFilesDir(null) + File.separator + srcPath;
+ if (assets != null) {
+ if (assets.length == 0) {
+ copyFile(context, srcPath, destPath);
+ } else {
+ File dir = new File(destPath);
+ if (!dir.exists()) dir.mkdirs();
+ for (String element : assets) {
+ copyAssetAll(context, srcPath + File.separator + element);
+ }
+ }
+ }
+ } catch (IOException ignored) {}
+ }
+
+ private static void copyFile(Context context, String srcFile, String destFile) {
+ AssetManager assetMgr = context.getAssets();
+ InputStream is = null;
+ FileOutputStream os = null;
+ try {
+ is = assetMgr.open(srcFile);
+ boolean exists = new File(destFile).exists();
+ if (srcFile.contains("shaders")) {
+ exists = false; // always refresh shaders
+ }
+ if (!exists) {
+ File parent = new File(destFile).getParentFile();
+ if (parent != null && !parent.exists()) parent.mkdirs();
+ os = new FileOutputStream(destFile);
+ byte[] buffer = new byte[4096];
+ int read;
+ while ((read = is.read(buffer)) != -1) {
+ os.write(buffer, 0, read);
+ }
+ os.flush();
+ }
+ } catch (IOException ignored) {
+ } finally {
+ try { if (is != null) is.close(); } catch (IOException ignored) {}
+ try { if (os != null) os.close(); } catch (IOException ignored) {}
+ }
+ }
+
+
+
+ @Override
+ public void onBackPressed() {
+ // Fallback for older Android versions
+ showExitDialog();
+ }
+
+ private void showExitDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("Exit App")
+ .setMessage("Do you want to exit PSX2?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton("Exit", (dialog, which) -> {
+ // Stop emulator first
+ NativeApp.shutdown();
+ // Quit the app
+ finishAffinity();
+ finishAndRemoveTask();
+ // As a fallback ensure process exit
+ System.exit(0);
+ })
+ .setNegativeButton("Cancel", null)
+ .show();
+ }
+}
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/NativeApp.java b/app/src/main/java/com/izzy2lost/psx2/NativeApp.java
similarity index 65%
rename from app/src/main/java/kr/co/iefriends/pcsx2/NativeApp.java
rename to app/src/main/java/com/izzy2lost/psx2/NativeApp.java
index 6f3e14a..676fe05 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/NativeApp.java
+++ b/app/src/main/java/com/izzy2lost/psx2/NativeApp.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.content.ContentResolver;
import android.content.Context;
@@ -58,6 +58,32 @@ public class NativeApp {
public static native void renderGpu(int value);
public static native void renderPreloading(int value);
+ // HUD/OSD visibility toggle
+ public static native void setHudVisible(boolean visible);
+
+ // Widescreen and interlacing patches
+ public static native void setWidescreenPatches(boolean enabled);
+ public static native void setNoInterlacingPatches(boolean enabled);
+
+ // Texture loading options for texture packs
+ public static native void setLoadTextures(boolean enabled);
+ public static native void setAsyncTextureLoading(boolean enabled);
+ public static native void setBlendingAccuracy(int level);
+
+ // Per-game settings
+ public static native void saveGameSettings(String filename, int blendingAccuracy, int renderer,
+ int resolution, boolean widescreenPatches,
+ boolean noInterlacingPatches, boolean enablePatches,
+ boolean enableCheats);
+ public static native void saveGameSettingsToPath(String fullPath, int blendingAccuracy, int renderer,
+ int resolution, boolean widescreenPatches,
+ boolean noInterlacingPatches, boolean enablePatches,
+ boolean enableCheats);
+ public static native void deleteGameSettings(String filename);
+ public static native String getGameSerial(String gameUri);
+ public static native String getGameCrc(String gameUri);
+ public static native String getCurrentGameSerial();
+
public static native void onNativeSurfaceCreated();
public static native void onNativeSurfaceChanged(Surface surface, int w, int h);
public static native void onNativeSurfaceDestroyed();
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/SDLControllerManager.java b/app/src/main/java/com/izzy2lost/psx2/SDLControllerManager.java
similarity index 99%
rename from app/src/main/java/kr/co/iefriends/pcsx2/SDLControllerManager.java
rename to app/src/main/java/com/izzy2lost/psx2/SDLControllerManager.java
index 1897911..00e985f 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/SDLControllerManager.java
+++ b/app/src/main/java/com/izzy2lost/psx2/SDLControllerManager.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.content.Context;
import android.os.Build;
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/SDLSurface.java b/app/src/main/java/com/izzy2lost/psx2/SDLSurface.java
similarity index 97%
rename from app/src/main/java/kr/co/iefriends/pcsx2/SDLSurface.java
rename to app/src/main/java/com/izzy2lost/psx2/SDLSurface.java
index 15753e0..cae0cb5 100644
--- a/app/src/main/java/kr/co/iefriends/pcsx2/SDLSurface.java
+++ b/app/src/main/java/com/izzy2lost/psx2/SDLSurface.java
@@ -1,4 +1,4 @@
-package kr.co.iefriends.pcsx2;
+package com.izzy2lost.psx2;
import android.content.Context;
import android.util.AttributeSet;
@@ -36,7 +36,6 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback {
@Override
public void surfaceChanged(@NonNull SurfaceHolder p_holder, int p_format, int p_width, int p_height) {
NativeApp.onNativeSurfaceChanged(p_holder.getSurface(), p_width, p_height);
- ////
MainActivity _nativeActivity = (MainActivity) getContext();
if(_nativeActivity != null) {
_nativeActivity.startEmuThread();
diff --git a/app/src/main/java/com/izzy2lost/psx2/SavesDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/SavesDialogFragment.java
new file mode 100644
index 0000000..28b79c5
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/SavesDialogFragment.java
@@ -0,0 +1,205 @@
+package com.izzy2lost.psx2;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class SavesDialogFragment extends DialogFragment {
+
+ public static class SaveSlot {
+ public int slot;
+ public String title;
+ public String timestamp;
+ public byte[] screenshot;
+ public boolean isEmpty;
+ public String gamePath;
+
+ public SaveSlot(int slot, String gamePath) {
+ this.slot = slot;
+ this.gamePath = gamePath;
+ this.isEmpty = true;
+ this.title = "Empty Slot " + slot;
+ this.timestamp = "";
+ }
+ }
+
+ private static class SaveSlotAdapter extends RecyclerView.Adapter {
+ private List saveSlots;
+ private OnSlotClickListener listener;
+
+ interface OnSlotClickListener {
+ void onSave(int slot);
+ void onLoad(int slot);
+ }
+
+ SaveSlotAdapter(List saveSlots, OnSlotClickListener listener) {
+ this.saveSlots = saveSlots;
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_save_slot, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ SaveSlot slot = saveSlots.get(position);
+ holder.bind(slot, listener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return saveSlots.size();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ TextView title, timestamp;
+ ImageView screenshot;
+ View saveButton, loadButton;
+
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ title = itemView.findViewById(R.id.tv_slot_title);
+ timestamp = itemView.findViewById(R.id.tv_slot_timestamp);
+ screenshot = itemView.findViewById(R.id.iv_slot_screenshot);
+ saveButton = itemView.findViewById(R.id.btn_slot_save);
+ loadButton = itemView.findViewById(R.id.btn_slot_load);
+ }
+
+ void bind(SaveSlot slot, OnSlotClickListener listener) {
+ title.setText(slot.title);
+ timestamp.setText(slot.timestamp);
+
+ // Set screenshot if available
+ if (slot.screenshot != null && slot.screenshot.length > 0) {
+ Bitmap bitmap = BitmapFactory.decodeByteArray(slot.screenshot, 0, slot.screenshot.length);
+ screenshot.setImageBitmap(bitmap);
+ screenshot.setVisibility(View.VISIBLE);
+
+ // Make screenshot clickable to show enlarged version
+ screenshot.setOnClickListener(v -> showEnlargedScreenshot(slot.screenshot, slot.title));
+ } else {
+ screenshot.setVisibility(View.GONE);
+ screenshot.setOnClickListener(null);
+ }
+
+ // Save button - always enabled
+ saveButton.setOnClickListener(v -> listener.onSave(slot.slot));
+
+ // Load button - only enabled if slot has data
+ loadButton.setEnabled(!slot.isEmpty);
+ loadButton.setAlpha(slot.isEmpty ? 0.5f : 1.0f);
+ loadButton.setOnClickListener(v -> {
+ if (!slot.isEmpty) {
+ listener.onLoad(slot.slot);
+ }
+ });
+ }
+
+ private void showEnlargedScreenshot(byte[] screenshotData, String title) {
+ if (screenshotData == null || screenshotData.length == 0) return;
+
+ Context context = itemView.getContext();
+ Bitmap bitmap = BitmapFactory.decodeByteArray(screenshotData, 0, screenshotData.length);
+
+ // Create enlarged screenshot dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_screenshot_preview, null);
+
+ ImageView enlargedImageView = dialogView.findViewById(R.id.iv_enlarged_screenshot);
+ TextView titleView = dialogView.findViewById(R.id.tv_screenshot_title);
+
+ enlargedImageView.setImageBitmap(bitmap);
+ titleView.setText(title);
+
+ builder.setView(dialogView)
+ .setPositiveButton("Close", (dialog, which) -> dialog.dismiss())
+ .create()
+ .show();
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ Context ctx = requireContext();
+ View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_saves, null, false);
+
+ RecyclerView recyclerView = view.findViewById(R.id.rv_save_slots);
+ recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
+
+ // Create save slots (1-10)
+ List saveSlots = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ SaveSlot slot = new SaveSlot(i, NativeApp.getGamePathSlot(i));
+
+ // Check if slot has data and get screenshot
+ byte[] screenshot = NativeApp.getImageSlot(i);
+ if (screenshot != null && screenshot.length > 0) {
+ slot.isEmpty = false;
+ slot.screenshot = screenshot;
+ slot.title = "Save Slot " + i;
+
+ // Create a reasonable timestamp (you might want to get this from native code)
+ slot.timestamp = "Saved " + SimpleDateFormat.getDateTimeInstance(
+ SimpleDateFormat.SHORT, SimpleDateFormat.SHORT, Locale.getDefault())
+ .format(new Date());
+ }
+
+ saveSlots.add(slot);
+ }
+
+ SaveSlotAdapter adapter = new SaveSlotAdapter(saveSlots, new SaveSlotAdapter.OnSlotClickListener() {
+ @Override
+ public void onSave(int slot) {
+ if (NativeApp.saveStateToSlot(slot)) {
+ // Success - refresh the dialog or close it
+ dismiss();
+ }
+ NativeApp.resume();
+ }
+
+ @Override
+ public void onLoad(int slot) {
+ if (NativeApp.loadStateFromSlot(slot)) {
+ // Success
+ dismiss();
+ }
+ NativeApp.resume();
+ }
+ });
+
+ recyclerView.setAdapter(adapter);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+ builder.setTitle("Save States")
+ .setView(view)
+ .setNegativeButton("Cancel", (d, w) -> d.dismiss());
+
+ return builder.create();
+ }
+}
diff --git a/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java
new file mode 100644
index 0000000..3d97a85
--- /dev/null
+++ b/app/src/main/java/com/izzy2lost/psx2/SettingsDialogFragment.java
@@ -0,0 +1,276 @@
+package com.izzy2lost.psx2;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.content.res.ColorStateList;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.core.widget.CompoundButtonCompat;
+import androidx.core.content.ContextCompat;
+
+public class SettingsDialogFragment extends DialogFragment {
+
+ private static final String PREFS = "app_prefs";
+ // Renderer constants (match native GSRendererType values used elsewhere)
+ private static final int RENDERER_OPENGL = 12;
+ private static final int RENDERER_SOFTWARE = 13;
+ private static final int RENDERER_VULKAN = 14;
+
+ // Static method to load and apply settings on app startup
+ public static void loadAndApplySettings(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+
+ int renderer = prefs.getInt("renderer", RENDERER_VULKAN);
+ float scale = prefs.getFloat("upscale_multiplier", 1.0f);
+ int aspectRatio = prefs.getInt("aspect_ratio", 1);
+ boolean widescreenPatches = prefs.getBoolean("widescreen_patches", false);
+ boolean noInterlacingPatches = prefs.getBoolean("no_interlacing_patches", false);
+ boolean loadTextures = prefs.getBoolean("load_textures", false);
+ boolean asyncTextureLoading = prefs.getBoolean("async_texture_loading", true);
+ boolean hudVisible = prefs.getBoolean("hud_visible", false);
+
+ // Apply all settings
+ NativeApp.renderGpu(renderer);
+ NativeApp.renderUpscalemultiplier(scale);
+ NativeApp.setAspectRatio(aspectRatio);
+ NativeApp.setWidescreenPatches(widescreenPatches);
+ NativeApp.setNoInterlacingPatches(noInterlacingPatches);
+ NativeApp.setLoadTextures(loadTextures);
+ NativeApp.setAsyncTextureLoading(asyncTextureLoading);
+ NativeApp.setHudVisible(hudVisible);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ Context ctx = requireContext();
+ View view = LayoutInflater.from(ctx).inflate(R.layout.dialog_settings, null, false);
+
+ RadioGroup rgRenderer = view.findViewById(R.id.rg_renderer);
+ RadioButton rbGl = view.findViewById(R.id.rb_renderer_gl);
+ RadioButton rbVk = view.findViewById(R.id.rb_renderer_vk);
+ RadioButton rbSw = view.findViewById(R.id.rb_renderer_sw);
+ Spinner spScale = view.findViewById(R.id.sp_scale);
+ Spinner spAspectRatio = view.findViewById(R.id.sp_aspect_ratio);
+ Switch swWidescreen = view.findViewById(R.id.sw_widescreen);
+ Switch swNoInterlacing = view.findViewById(R.id.sw_no_interlacing);
+ Switch swLoadTextures = view.findViewById(R.id.sw_load_textures);
+ Switch swAsyncTextureLoading = view.findViewById(R.id.sw_async_texture_loading);
+ Switch swDevHud = view.findViewById(R.id.sw_dev_hud);
+ View btnPower = view.findViewById(R.id.btn_power);
+ View btnReboot = view.findViewById(R.id.btn_reboot);
+
+ // Brand tints for checked/activated states to replace aqua
+ int brand = ContextCompat.getColor(ctx, R.color.brand_primary);
+ int outline = ContextCompat.getColor(ctx, R.color.brand_outline);
+ int[][] states = new int[][]{
+ new int[]{android.R.attr.state_checked},
+ new int[]{}
+ };
+ int[] colors = new int[]{
+ brand,
+ outline
+ };
+ ColorStateList brandChecked = new ColorStateList(states, colors);
+
+ // RadioButtons
+ if (rbGl != null) CompoundButtonCompat.setButtonTintList(rbGl, brandChecked);
+ if (rbVk != null) CompoundButtonCompat.setButtonTintList(rbVk, brandChecked);
+ if (rbSw != null) CompoundButtonCompat.setButtonTintList(rbSw, brandChecked);
+
+ // Switches (thumb = brand when checked; track = subtle brand when checked)
+ ColorStateList thumbTint = new ColorStateList(
+ new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
+ new int[]{brand, outline}
+ );
+ int brandTrack = ColorStateList.valueOf(brand).withAlpha(100).getDefaultColor();
+ int outlineTrack = ColorStateList.valueOf(outline).withAlpha(80).getDefaultColor();
+ ColorStateList trackTint = new ColorStateList(
+ new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
+ new int[]{brandTrack, outlineTrack}
+ );
+ if (swWidescreen != null) {
+ swWidescreen.setThumbTintList(thumbTint);
+ swWidescreen.setTrackTintList(trackTint);
+ }
+ if (swNoInterlacing != null) {
+ swNoInterlacing.setThumbTintList(thumbTint);
+ swNoInterlacing.setTrackTintList(trackTint);
+ }
+ if (swLoadTextures != null) {
+ swLoadTextures.setThumbTintList(thumbTint);
+ swLoadTextures.setTrackTintList(trackTint);
+ }
+ if (swAsyncTextureLoading != null) {
+ swAsyncTextureLoading.setThumbTintList(thumbTint);
+ swAsyncTextureLoading.setTrackTintList(trackTint);
+ }
+ if (swDevHud != null) {
+ swDevHud.setThumbTintList(thumbTint);
+ swDevHud.setTrackTintList(trackTint);
+ }
+
+ if (btnPower != null) {
+ btnPower.setOnClickListener(v -> {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Power Off")
+ .setMessage("Quit the app?")
+ .setNegativeButton("Cancel", null)
+ .setPositiveButton("Quit", (d1, w1) -> {
+ // Stop emulator first
+ NativeApp.shutdown();
+ // Close dialog
+ dismissAllowingStateLoss();
+ // Quit the whole app/activity task
+ if (getActivity() != null) {
+ getActivity().finishAffinity();
+ getActivity().finishAndRemoveTask();
+ }
+ // As a fallback ensure process exit
+ System.exit(0);
+ })
+ .show();
+ });
+ }
+
+ if (btnReboot != null) {
+ btnReboot.setOnClickListener(v -> {
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Reboot")
+ .setMessage("Restart the current game?")
+ .setNegativeButton("Cancel", null)
+ .setPositiveButton("Reboot", (d1, w1) -> {
+ if (requireActivity() instanceof MainActivity) {
+ ((MainActivity) requireActivity()).rebootEmu();
+ } else {
+ NativeApp.shutdown();
+ }
+ dismissAllowingStateLoss();
+ })
+ .show();
+ });
+ }
+
+ // Populate scale spinner (1x..8x)
+ ArrayAdapter scaleAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.scale_entries, android.R.layout.simple_spinner_item);
+ scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spScale.setAdapter(scaleAdapter);
+
+ // Populate aspect ratio spinner
+ ArrayAdapter aspectAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.aspect_ratio_entries, android.R.layout.simple_spinner_item);
+ aspectAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ spAspectRatio.setAdapter(aspectAdapter);
+
+ SharedPreferences prefs = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
+ int savedRenderer = prefs.getInt("renderer", RENDERER_VULKAN);
+ float savedScale = prefs.getFloat("upscale_multiplier", 1.0f);
+ int savedAspectRatio = prefs.getInt("aspect_ratio", 1); // 1 = Auto 4:3/3:2 (recommended)
+ boolean savedWidescreen = prefs.getBoolean("widescreen_patches", false);
+ boolean savedNoInterlacing = prefs.getBoolean("no_interlacing_patches", false);
+ boolean savedLoadTextures = prefs.getBoolean("load_textures", false);
+ boolean savedAsyncTextureLoading = prefs.getBoolean("async_texture_loading", true);
+ boolean savedHud = prefs.getBoolean("hud_visible", false);
+
+ if (savedRenderer == RENDERER_VULKAN) rbVk.setChecked(true);
+ else if (savedRenderer == RENDERER_SOFTWARE) rbSw.setChecked(true);
+ else rbGl.setChecked(true);
+
+ int scaleIndex = scaleToIndex(savedScale);
+ if (scaleIndex < 0 || scaleIndex >= scaleAdapter.getCount()) scaleIndex = 0;
+ spScale.setSelection(scaleIndex);
+
+ if (savedAspectRatio >= 0 && savedAspectRatio < aspectAdapter.getCount()) {
+ spAspectRatio.setSelection(savedAspectRatio);
+ } else {
+ spAspectRatio.setSelection(1); // Default to Auto 4:3/3:2
+ }
+
+ swWidescreen.setChecked(savedWidescreen);
+ swNoInterlacing.setChecked(savedNoInterlacing);
+ swLoadTextures.setChecked(savedLoadTextures);
+ swAsyncTextureLoading.setChecked(savedAsyncTextureLoading);
+ if (swDevHud != null) swDevHud.setChecked(savedHud);
+
+ AlertDialog.Builder b = new AlertDialog.Builder(requireContext());
+ b.setTitle("Graphics Settings")
+ .setView(view)
+ .setNegativeButton("Cancel", (d, w) -> d.dismiss())
+ .setPositiveButton("Save", (d, w) -> {
+ int renderer = RENDERER_OPENGL;
+ int checked = rgRenderer.getCheckedRadioButtonId();
+ if (checked == R.id.rb_renderer_vk) renderer = RENDERER_VULKAN;
+ else if (checked == R.id.rb_renderer_sw) renderer = RENDERER_SOFTWARE;
+
+ float scale = indexToScale(spScale.getSelectedItemPosition());
+ int aspectRatio = spAspectRatio.getSelectedItemPosition();
+ boolean widescreenPatches = swWidescreen.isChecked();
+ boolean noInterlacingPatches = swNoInterlacing.isChecked();
+ boolean loadTextures = swLoadTextures.isChecked();
+ boolean asyncTextureLoading = swAsyncTextureLoading.isChecked();
+ boolean hudVisible = (swDevHud != null && swDevHud.isChecked());
+
+ // Persist
+ prefs.edit()
+ .putInt("renderer", renderer)
+ .putFloat("upscale_multiplier", scale)
+ .putInt("aspect_ratio", aspectRatio)
+ .putBoolean("widescreen_patches", widescreenPatches)
+ .putBoolean("no_interlacing_patches", noInterlacingPatches)
+ .putBoolean("load_textures", loadTextures)
+ .putBoolean("async_texture_loading", asyncTextureLoading)
+ .putBoolean("hud_visible", hudVisible)
+ .apply();
+
+ // Apply immediately
+ NativeApp.renderGpu(renderer);
+ NativeApp.renderUpscalemultiplier(scale);
+ NativeApp.setAspectRatio(aspectRatio);
+ NativeApp.setWidescreenPatches(widescreenPatches);
+ NativeApp.setNoInterlacingPatches(noInterlacingPatches);
+ NativeApp.setLoadTextures(loadTextures);
+ NativeApp.setAsyncTextureLoading(asyncTextureLoading);
+ NativeApp.setHudVisible(hudVisible);
+ });
+
+ return b.create();
+ }
+
+ private static int scaleToIndex(float scale) {
+ if (scale <= 1.0f) return 0;
+ if (scale <= 2.0f) return 1;
+ if (scale <= 3.0f) return 2;
+ if (scale <= 4.0f) return 3;
+ if (scale <= 5.0f) return 4;
+ if (scale <= 6.0f) return 5;
+ if (scale <= 7.0f) return 6;
+ return 7; // 8x or higher
+ }
+
+ private static float indexToScale(int index) {
+ switch (index) {
+ case 1: return 2.0f;
+ case 2: return 3.0f;
+ case 3: return 4.0f;
+ case 4: return 5.0f;
+ case 5: return 6.0f;
+ case 6: return 7.0f;
+ case 7: return 8.0f;
+ case 0:
+ default: return 1.0f;
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/iefriends/pcsx2/MainActivity.java b/app/src/main/java/kr/co/iefriends/pcsx2/MainActivity.java
deleted file mode 100644
index c9a982c..0000000
--- a/app/src/main/java/kr/co/iefriends/pcsx2/MainActivity.java
+++ /dev/null
@@ -1,539 +0,0 @@
-package kr.co.iefriends.pcsx2;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.AssetManager;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-
-import com.google.android.material.button.MaterialButton;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-public class MainActivity extends AppCompatActivity {
- private String m_szGamefile = "";
-
- private HIDDeviceManager mHIDDeviceManager;
- private Thread mEmulationThread = null;
-
- private boolean isThread() {
- if (mEmulationThread != null) {
- Thread.State _thread_state = mEmulationThread.getState();
- return _thread_state == Thread.State.BLOCKED
- || _thread_state == Thread.State.RUNNABLE
- || _thread_state == Thread.State.TIMED_WAITING
- || _thread_state == Thread.State.WAITING;
- }
- return false;
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- // Default resources
- copyAssetAll(getApplicationContext(), "bios");
- copyAssetAll(getApplicationContext(), "resources");
-
- Initialize();
-
- makeButtonTouch();
-
- setSurfaceView(new SDLSurface(this));
- }
-
- // Buttons
- private void makeButtonTouch() {
- // Game file
- MaterialButton btn_file = findViewById(R.id.btn_file);
- if(btn_file != null) {
- btn_file.setOnClickListener(v -> {
- // Internal storage
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
- intent.setType("*/*");
- startActivityResultLocalFilePlay.launch(intent);
- });
- }
-
- // Game save
- MaterialButton btn_save = findViewById(R.id.btn_save);
- if(btn_save != null) {
- btn_save.setOnClickListener(v -> {
- if(NativeApp.saveStateToSlot(1)) {
- // Success
- } else {
- // Failed
- }
- NativeApp.resume();
- });
- }
-
- // Game load
- MaterialButton btn_load = findViewById(R.id.btn_load);
- if(btn_load != null) {
- btn_load.setOnClickListener(v -> {
- if(NativeApp.loadStateFromSlot(1)) {
- // Success
- } else {
- // Failed
- }
- NativeApp.resume();
- });
- }
-
- //////
- // RENDERER
-
- MaterialButton btn_ogl = findViewById(R.id.btn_ogl);
- if(btn_ogl != null) {
- btn_ogl.setOnClickListener(v -> {
- NativeApp.renderGpu(12);
- });
- }
- MaterialButton btn_vulkan = findViewById(R.id.btn_vulkan);
- if(btn_vulkan != null) {
- btn_vulkan.setOnClickListener(v -> {
- NativeApp.renderGpu(14);
- });
- }
- MaterialButton btn_sw = findViewById(R.id.btn_sw);
- if(btn_sw != null) {
- btn_sw.setOnClickListener(v -> {
- NativeApp.renderGpu(13);
- });
- }
-
- //////
- // PAD
-
- MaterialButton btn_pad_select = findViewById(R.id.btn_pad_select);
- if(btn_pad_select != null) {
- btn_pad_select.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_SELECT);
- return true;
- });
- }
- MaterialButton btn_pad_start = findViewById(R.id.btn_pad_start);
- if(btn_pad_start != null) {
- btn_pad_start.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_START);
- return true;
- });
- }
-
- MaterialButton btn_pad_a = findViewById(R.id.btn_pad_a);
- if(btn_pad_a != null) {
- btn_pad_a.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_A);
- return true;
- });
- }
- MaterialButton btn_pad_b = findViewById(R.id.btn_pad_b);
- if(btn_pad_b != null) {
- btn_pad_b.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_B);
- return true;
- });
- }
- MaterialButton btn_pad_x = findViewById(R.id.btn_pad_x);
- if(btn_pad_x != null) {
- btn_pad_x.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_X);
- return true;
- });
- }
- MaterialButton btn_pad_y = findViewById(R.id.btn_pad_y);
- if(btn_pad_y != null) {
- btn_pad_y.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_Y);
- return true;
- });
- }
-
- ////
-
- MaterialButton btn_pad_l1 = findViewById(R.id.btn_pad_l1);
- if(btn_pad_l1 != null) {
- btn_pad_l1.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L1);
- return true;
- });
- }
- MaterialButton btn_pad_r1 = findViewById(R.id.btn_pad_r1);
- if(btn_pad_r1 != null) {
- btn_pad_r1.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R1);
- return true;
- });
- }
-
- MaterialButton btn_pad_l2 = findViewById(R.id.btn_pad_l2);
- if(btn_pad_l2 != null) {
- btn_pad_l2.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_L2);
- return true;
- });
- }
- MaterialButton btn_pad_r2 = findViewById(R.id.btn_pad_r2);
- if(btn_pad_r2 != null) {
- btn_pad_r2.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_R2);
- return true;
- });
- }
-
- MaterialButton btn_pad_l3 = findViewById(R.id.btn_pad_l3);
- if(btn_pad_l3 != null) {
- btn_pad_l3.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBL);
- return true;
- });
- }
- MaterialButton btn_pad_r3 = findViewById(R.id.btn_pad_r3);
- if(btn_pad_r3 != null) {
- btn_pad_r3.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_BUTTON_THUMBR);
- return true;
- });
- }
-
- ////
-
- final int PAD_L_UP = 110;
- final int PAD_L_RIGHT = 111;
- final int PAD_L_DOWN = 112;
- final int PAD_L_LEFT = 113;
-
- final int PAD_R_UP = 120;
- final int PAD_R_RIGHT = 121;
- final int PAD_R_DOWN = 122;
- final int PAD_R_LEFT = 123;
-
- MaterialButton btn_pad_joy_lt = findViewById(R.id.btn_pad_joy_lt);
- if(btn_pad_joy_lt != null) {
- btn_pad_joy_lt.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_UP);
- sendKeyAction(v, event.getAction(), PAD_L_LEFT);
- return true;
- });
- }
- MaterialButton btn_pad_joy_t = findViewById(R.id.btn_pad_joy_t);
- if(btn_pad_joy_t != null) {
- btn_pad_joy_t.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_UP);
- return true;
- });
- }
- MaterialButton btn_pad_joy_rt = findViewById(R.id.btn_pad_joy_rt);
- if(btn_pad_joy_rt != null) {
- btn_pad_joy_rt.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_UP);
- sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
- return true;
- });
- }
- MaterialButton btn_pad_joy_l = findViewById(R.id.btn_pad_joy_l);
- if(btn_pad_joy_l != null) {
- btn_pad_joy_l.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_LEFT);
- return true;
- });
- }
- MaterialButton btn_pad_joy_r = findViewById(R.id.btn_pad_joy_r);
- if(btn_pad_joy_r != null) {
- btn_pad_joy_r.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
- return true;
- });
- }
- MaterialButton btn_pad_joy_lb = findViewById(R.id.btn_pad_joy_lb);
- if(btn_pad_joy_lb != null) {
- btn_pad_joy_lb.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_LEFT);
- sendKeyAction(v, event.getAction(), PAD_L_DOWN);
- return true;
- });
- }
- MaterialButton btn_pad_joy_b = findViewById(R.id.btn_pad_joy_b);
- if(btn_pad_joy_b != null) {
- btn_pad_joy_b.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_DOWN);
- return true;
- });
- }
- MaterialButton btn_pad_joy_rb = findViewById(R.id.btn_pad_joy_rb);
- if(btn_pad_joy_rb != null) {
- btn_pad_joy_rb.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), PAD_L_RIGHT);
- sendKeyAction(v, event.getAction(), PAD_L_DOWN);
- return true;
- });
- }
-
- ////
-
- MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top);
- if(btn_pad_dir_top != null) {
- btn_pad_dir_top.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_UP);
- return true;
- });
- }
- MaterialButton btn_pad_dir_bottom = findViewById(R.id.btn_pad_dir_bottom);
- if(btn_pad_dir_bottom != null) {
- btn_pad_dir_bottom.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_DOWN);
- return true;
- });
- }
- MaterialButton btn_pad_dir_left = findViewById(R.id.btn_pad_dir_left);
- if(btn_pad_dir_left != null) {
- btn_pad_dir_left.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_LEFT);
- return true;
- });
- }
- MaterialButton btn_pad_dir_right = findViewById(R.id.btn_pad_dir_right);
- if(btn_pad_dir_right != null) {
- btn_pad_dir_right.setOnTouchListener((v, event) -> {
- sendKeyAction(v, event.getAction(), KeyEvent.KEYCODE_DPAD_RIGHT);
- return true;
- });
- }
- }
-
- public final ActivityResultLauncher startActivityResultLocalFilePlay = registerForActivityResult(
- new ActivityResultContracts.StartActivityForResult(),
- result -> {
- if(result.getResultCode() == Activity.RESULT_OK) {
- try {
- Intent _intent = result.getData();
- if(_intent != null) {
- m_szGamefile = _intent.getDataString();
- if(!TextUtils.isEmpty(m_szGamefile)) {
- restartEmuThread();
- }
- }
- } catch (Exception ignored) {}
- }
- });
-
- @Override
- public void onConfigurationChanged(@NonNull Configuration p_newConfig) {
- super.onConfigurationChanged(p_newConfig);
- }
-
- @Override
- protected void onPause() {
- NativeApp.pause();
- super.onPause();
- ////
- if (mHIDDeviceManager != null) {
- mHIDDeviceManager.setFrozen(true);
- }
- }
-
- @Override
- protected void onResume() {
- NativeApp.resume();
- super.onResume();
- ////
- if (mHIDDeviceManager != null) {
- mHIDDeviceManager.setFrozen(false);
- }
- }
-
- @Override
- protected void onDestroy() {
- NativeApp.shutdown();
- super.onDestroy();
- ////
- if (mHIDDeviceManager != null) {
- HIDDeviceManager.release(mHIDDeviceManager);
- mHIDDeviceManager = null;
- }
- ////
- if (mEmulationThread != null) {
- try {
- mEmulationThread.join();
- mEmulationThread = null;
- }
- catch (InterruptedException ignored) {}
- }
-
- int appPid = android.os.Process.myPid();
- android.os.Process.killProcess(appPid);
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////////
-
- public void Initialize() {
- NativeApp.initializeOnce(getApplicationContext());
-
- // Set up JNI
- SDLControllerManager.nativeSetupJNI();
-
- // Initialize state
- SDLControllerManager.initialize();
-
- mHIDDeviceManager = HIDDeviceManager.acquire(this);
- }
-
- private void setSurfaceView(Object p_value) {
- FrameLayout fl_board = findViewById(R.id.fl_board);
- if(fl_board != null) {
- if(fl_board.getChildCount() > 0) {
- fl_board.removeAllViews();
- }
- ////
- if(p_value instanceof SDLSurface) {
- fl_board.addView((SDLSurface)p_value);
- }
- }
- }
-
- public void startEmuThread() {
- if(!isThread()) {
- mEmulationThread = new Thread(() -> NativeApp.runVMThread(m_szGamefile));
- mEmulationThread.start();
- }
- }
-
- private void restartEmuThread() {
- NativeApp.shutdown();
- if (mEmulationThread != null) {
- try {
- mEmulationThread.join();
- mEmulationThread = null;
- }
- catch (InterruptedException ignored) {}
- }
- ////
- startEmuThread();
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////////
-
- @Override
- public boolean onGenericMotionEvent(MotionEvent event) {
- if (SDLControllerManager.isDeviceSDLJoystick(event.getDeviceId())) {
- SDLControllerManager.handleJoystickMotionEvent(event);
- return true;
- }
- return super.onGenericMotionEvent(event);
- }
-
- @Override
- public boolean onKeyDown(int p_keyCode, KeyEvent p_event) {
- if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
- if (p_event.getRepeatCount() == 0) {
- SDLControllerManager.onNativePadDown(p_event.getDeviceId(), p_keyCode);
- return true;
- }
- }
- else {
- if (p_keyCode == KeyEvent.KEYCODE_BACK) {
- finish();
- return true;
- }
- }
- return super.onKeyDown(p_keyCode, p_event);
- }
-
- @Override
- public boolean onKeyUp(int p_keyCode, KeyEvent p_event) {
- if ((p_event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
- if (p_event.getRepeatCount() == 0) {
- SDLControllerManager.onNativePadUp(p_event.getDeviceId(), p_keyCode);
- return true;
- }
- }
- return super.onKeyUp(p_keyCode, p_event);
- }
-
- public static void sendKeyAction(View p_view, int p_action, int p_keycode) {
- if(p_action == MotionEvent.ACTION_DOWN) {
- p_view.setPressed(true);
- int pad_force = 0;
- if(p_keycode >= 110) {
- float _abs = 90; // Joystic test value
- _abs = Math.min(_abs, 100);
- pad_force = (int) (_abs * 32766.0f / 100);
- }
- NativeApp.setPadButton(p_keycode, pad_force, true);
- } else if(p_action == MotionEvent.ACTION_UP || p_action == MotionEvent.ACTION_CANCEL) {
- p_view.setPressed(false);
- NativeApp.setPadButton(p_keycode, 0, false);
- }
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////////
-
- public static void copyAssetAll(Context p_context, String srcPath) {
- AssetManager assetMgr = p_context.getAssets();
- String[] assets = null;
- try {
- String destPath = p_context.getExternalFilesDir(null) + File.separator + srcPath;
- assets = assetMgr.list(srcPath);
- if(assets != null) {
- if (assets.length == 0) {
- copyFile(p_context, srcPath, destPath);
- } else {
- File dir = new File(destPath);
- if (!dir.exists())
- dir.mkdir();
- for (String element : assets) {
- copyAssetAll(p_context, srcPath + File.separator + element);
- }
- }
- }
- }
- catch (IOException ignored) {}
- }
-
- public static void copyFile(Context p_context, String srcFile, String destFile) {
- AssetManager assetMgr = p_context.getAssets();
-
- InputStream is = null;
- FileOutputStream os = null;
- try {
- is = assetMgr.open(srcFile);
- boolean _exists = new File(destFile).exists();
- if(srcFile.contains("shaders")) {
- _exists = false;
- }
- if(!_exists)
- {
- os = new FileOutputStream(destFile);
-
- byte[] buffer = new byte[1024];
- int read;
- while ((read = is.read(buffer)) != -1) {
- os.write(buffer, 0, read);
- }
- is.close();
- os.flush();
- os.close();
- }
- }
- catch (IOException ignored) {}
- }
-}
diff --git a/app/src/main/res/drawable/bg_control_cluster.xml b/app/src/main/res/drawable/bg_control_cluster.xml
new file mode 100644
index 0000000..7640605
--- /dev/null
+++ b/app/src/main/res/drawable/bg_control_cluster.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_joystick.xml b/app/src/main/res/drawable/bg_joystick.xml
new file mode 100644
index 0000000..0e3f976
--- /dev/null
+++ b/app/src/main/res/drawable/bg_joystick.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_joystick_knob.xml b/app/src/main/res/drawable/bg_joystick_knob.xml
new file mode 100644
index 0000000..026cd41
--- /dev/null
+++ b/app/src/main/res/drawable/bg_joystick_knob.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml
new file mode 100644
index 0000000..e53fb22
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_down.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_left.xml b/app/src/main/res/drawable/ic_arrow_left.xml
new file mode 100644
index 0000000..281f673
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_left.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 0000000..8a6d198
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml
new file mode 100644
index 0000000..ab9d711
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_up.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 0000000..09c6fe7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 0000000..492686d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 2b068d1..c5c88b3 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -2,29 +2,215 @@
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
- android:viewportWidth="108"
- android:viewportHeight="108">
-
-
-
-
-
-
-
+ android:viewportWidth="512"
+ android:viewportHeight="512">
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+ android:pathData="M53,173.2L116,173.2L116,218.2L53,218.2"
+ android:strokeLineJoin="round"
+ android:strokeWidth="8.1"
+ android:fillColor="#00000000"
+ android:strokeLineCap="round">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_power.xml b/app/src/main/res/drawable/ic_power.xml
new file mode 100644
index 0000000..9d882b6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_power.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_ps2_circle.xml b/app/src/main/res/drawable/ic_ps2_circle.xml
new file mode 100644
index 0000000..bdb5e48
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ps2_circle.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_ps2_square.xml b/app/src/main/res/drawable/ic_ps2_square.xml
new file mode 100644
index 0000000..6aa9e3b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ps2_square.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_ps2_triangle.xml b/app/src/main/res/drawable/ic_ps2_triangle.xml
new file mode 100644
index 0000000..33ae793
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ps2_triangle.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_ps2_x.xml b/app/src/main/res/drawable/ic_ps2_x.xml
new file mode 100644
index 0000000..36b8caa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ps2_x.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_reboot.xml b/app/src/main/res/drawable/ic_reboot.xml
new file mode 100644
index 0000000..247aa21
--- /dev/null
+++ b/app/src/main/res/drawable/ic_reboot.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_unhide.xml b/app/src/main/res/drawable/ic_unhide.xml
new file mode 100644
index 0000000..b522953
--- /dev/null
+++ b/app/src/main/res/drawable/ic_unhide.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/psx2_logo2_fixed.xml b/app/src/main/res/drawable/psx2_logo2_fixed.xml
new file mode 100644
index 0000000..9131205
--- /dev/null
+++ b/app/src/main/res/drawable/psx2_logo2_fixed.xml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 0000000..313d61a
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,468 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/dialog_covers_grid.xml b/app/src/main/res/layout-land/dialog_covers_grid.xml
new file mode 100644
index 0000000..b7bf474
--- /dev/null
+++ b/app/src/main/res/layout-land/dialog_covers_grid.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-port/activity_main.xml b/app/src/main/res/layout-port/activity_main.xml
new file mode 100644
index 0000000..d773c97
--- /dev/null
+++ b/app/src/main/res/layout-port/activity_main.xml
@@ -0,0 +1,508 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-port/dialog_covers_grid.xml b/app/src/main/res/layout-port/dialog_covers_grid.xml
new file mode 100644
index 0000000..b7bf474
--- /dev/null
+++ b/app/src/main/res/layout-port/dialog_covers_grid.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f5a4a0e..48bf983 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -12,12 +12,27 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
+
+
+
@@ -55,6 +70,54 @@
app:strokeColor="#80000000" />
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/tv_long_press_hint">
+
+
@@ -223,8 +300,13 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="▢"
- android:textColor="@color/white"
+ android:text=""
+ android:gravity="center"
+ app:icon="@drawable/ic_ps2_square"
+ app:iconTint="#FF80C0"
+ app:iconPadding="0dp"
+ app:iconSize="20dp"
+ app:iconGravity="textStart"
app:rippleColor="#80cdcdcd"
app:strokeColor="#80000000" />
@@ -233,8 +315,13 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="△"
- android:textColor="@color/white"
+ android:text=""
+ android:gravity="center"
+ app:icon="@drawable/ic_ps2_triangle"
+ app:iconTint="#00D000"
+ app:iconPadding="0dp"
+ app:iconSize="20dp"
+ app:iconGravity="textStart"
app:rippleColor="#80cdcdcd"
app:strokeColor="#80000000" />
@@ -249,8 +336,13 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="⨉"
- android:textColor="@color/white"
+ android:text=""
+ android:gravity="center"
+ app:icon="@drawable/ic_ps2_x"
+ app:iconTint="#4080FF"
+ app:iconPadding="0dp"
+ app:iconSize="20dp"
+ app:iconGravity="textStart"
app:rippleColor="#80cdcdcd"
app:strokeColor="#80000000" />
@@ -259,8 +351,13 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="○"
- android:textColor="@color/white"
+ android:text=""
+ android:gravity="center"
+ app:icon="@drawable/ic_ps2_circle"
+ app:iconTint="#FF3030"
+ app:iconPadding="0dp"
+ app:iconSize="20dp"
+ app:iconGravity="textStart"
app:rippleColor="#80cdcdcd"
app:strokeColor="#80000000" />
@@ -272,6 +369,7 @@
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
+ android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
@@ -388,11 +486,13 @@
@@ -410,7 +510,8 @@
android:text="↑"
android:textColor="@color/white"
app:rippleColor="#80cdcdcd"
- app:strokeColor="#80000000" />
+ app:strokeWidth="0dp"
+ app:strokeColor="@android:color/transparent" />
+ app:strokeWidth="0dp"
+ app:strokeColor="@android:color/transparent" />
+ app:strokeWidth="0dp"
+ app:strokeColor="@android:color/transparent" />
+ app:strokeWidth="0dp"
+ app:strokeColor="@android:color/transparent" />
diff --git a/app/src/main/res/layout/dialog_covers_grid.xml b/app/src/main/res/layout/dialog_covers_grid.xml
new file mode 100644
index 0000000..500b4c2
--- /dev/null
+++ b/app/src/main/res/layout/dialog_covers_grid.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_game_settings.xml b/app/src/main/res/layout/dialog_game_settings.xml
new file mode 100644
index 0000000..a5a2569
--- /dev/null
+++ b/app/src/main/res/layout/dialog_game_settings.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_saves.xml b/app/src/main/res/layout/dialog_saves.xml
new file mode 100644
index 0000000..e64c02a
--- /dev/null
+++ b/app/src/main/res/layout/dialog_saves.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_screenshot_preview.xml b/app/src/main/res/layout/dialog_screenshot_preview.xml
new file mode 100644
index 0000000..8ea081b
--- /dev/null
+++ b/app/src/main/res/layout/dialog_screenshot_preview.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_settings.xml b/app/src/main/res/layout/dialog_settings.xml
new file mode 100644
index 0000000..f988ab5
--- /dev/null
+++ b/app/src/main/res/layout/dialog_settings.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_cover.xml b/app/src/main/res/layout/item_cover.xml
new file mode 100644
index 0000000..5f41dd7
--- /dev/null
+++ b/app/src/main/res/layout/item_cover.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_save_slot.xml b/app/src/main/res/layout/item_save_slot.xml
new file mode 100644
index 0000000..1268cc0
--- /dev/null
+++ b/app/src/main/res/layout/item_save_slot.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 56%
rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml
rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 6f3b755..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 56%
rename from app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755..7353dbd 100644
--- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..4c3a02d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..6eed199
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..f82c3f7
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..86a23ae
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..8bbdedb
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8fc76ab
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8d70a30
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..01b751a
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..898047d
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..5211892
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index 9281ae2..fa51bf4 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,6 +1,6 @@
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..35db0ec
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,37 @@
+
+
+
+ - 1x (Native ~480p)
+ - 2x (~720p HD)
+ - 3x (~1080p FHD)
+ - 4x (~1440p QHD)
+ - 5x (~1800p QHD+)
+ - 6x (~2160p 4K UHD)
+ - 7x (~2520p)
+ - 8x (~2880p 5K UHD)
+
+
+
+ - Stretch (Fill Screen)
+ - Auto 4:3/3:2 (Recommended)
+ - 4:3 (Original PS2)
+ - 16:9 (Widescreen)
+ - 10:7 (Uncommon)
+
+
+
+ - Minimum (Fastest, may break effects)
+ - Basic (Recommended)
+ - Medium (Good compatibility)
+ - High (Better effects)
+ - Full (Best quality, slower)
+ - Maximum (Perfect, very slow)
+
+
+
+ - Auto (Recommended)
+ - Hardware (Vulkan)
+ - Hardware (OpenGL)
+ - Software (Slow, accurate)
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f8c6127..78b6cb4 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,17 @@
#FF018786
#FF000000
#FFFFFFFF
+
+
+ #33A1FF
+ #7C52FF
+ #66CCFF
+ #8F61FF
+ #8033A1FF
+
+
+ #00D000
+ #FF3030
+ #4080FF
+ #FF80C0
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..beab31f
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6cff8d8..2f30268 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
- PCSX2
-
\ No newline at end of file
+ PSX2
+ Long press a game cover for per-game settings
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index b1aa576..868e007 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,8 +1,22 @@
-
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c..d64cd49 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f5059fc..1af9e09 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Sun May 12 00:33:42 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 4f906e0..1aa94a4 100644
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,99 @@
#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=`expr $i + 1`
- done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 107acd3..93e3f59 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/settings.gradle b/settings.gradle
index cb5199e..cad152a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,5 +13,5 @@ dependencyResolutionManagement {
}
}
-rootProject.name = "PCSX2"
+rootProject.name = "PSX2"
include ':app'