mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
ui changes cover downloader etc.
This commit is contained in:
@@ -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.
|
||||
+7
-3
@@ -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'
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
android:installLocation="preferExternal"
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.screen.landscape" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
@@ -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">
|
||||
|
||||
<meta-data android:name="android.max_aspect" android:value="2.4" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
#include <unistd.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
+469
-35
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,10 @@
|
||||
#include "fmt/format.h"
|
||||
#include "xxhash.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
static constexpr u32 MAX_PARENTS = 32; // Surely someone wouldn't be insane enough to go beyond this...
|
||||
static std::vector<std::pair<std::string, chd_header>> s_chd_hash_cache; // <filename, header>
|
||||
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;
|
||||
|
||||
@@ -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<microProgram*> 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<microRange>();
|
||||
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<microRange>();
|
||||
prog->startPC = startPC;
|
||||
if(doWholeProgCompare)
|
||||
mVUcacheProg(mVU, *prog); // Cache Micro Program
|
||||
|
||||
@@ -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<microBlockLinkRef> 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;
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -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<CoversAdapter.VH> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CharSequence> 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<CharSequence> 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<CharSequence> 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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.iefriends.pcsx2;
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.hardware.usb.UsbDevice;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.iefriends.pcsx2;
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.iefriends.pcsx2;
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.iefriends.pcsx2;
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+27
-1
@@ -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();
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kr.co.iefriends.pcsx2;
|
||||
package com.izzy2lost.psx2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user