ui changes cover downloader etc.

This commit is contained in:
izzy2lost
2025-08-22 04:11:11 -04:00
parent e7b7386c1d
commit 2160e64be1
87 changed files with 6032 additions and 934 deletions
-142
View File
@@ -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(&microVU0.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
View File
@@ -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'
}
+5 -3
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff
+38 -8
View File
@@ -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;
+32 -10
View File
@@ -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
+42 -7
View File
@@ -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,4 +1,4 @@
package kr.co.iefriends.pcsx2;
package com.izzy2lost.psx2;
import android.hardware.usb.UsbDevice;
@@ -1,4 +1,4 @@
package kr.co.iefriends.pcsx2;
package com.izzy2lost.psx2;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
@@ -1,4 +1,4 @@
package kr.co.iefriends.pcsx2;
package com.izzy2lost.psx2;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
@@ -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
@@ -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,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