diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1d6dfa9f38..ede9022327 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,5 +43,10 @@ android:screenOrientation="fullSensor" android:exported="false" /> + + diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index a2b1f4f99f..4939507195 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -329,6 +329,80 @@ cleanup: return out; } +static bool GetPrefBool(JNIEnv* env, jobject activity, const char* key, bool defValue) { + if (!env || !activity || !key || key[0] == '\0') { + return defValue; + } + bool out = defValue; + jclass activityClass = env->GetObjectClass(activity); + if (!activityClass) return defValue; + jmethodID getPrefs = env->GetMethodID(activityClass, "getSharedPreferences", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences;"); + env->DeleteLocalRef(activityClass); + if (!getPrefs) return defValue; + jstring prefsName = env->NewStringUTF(kPrefsName); + if (!prefsName) return defValue; + jobject prefs = env->CallObjectMethod(activity, getPrefs, prefsName, 0); + env->DeleteLocalRef(prefsName); + if (HasException(env, "getSharedPreferences") || !prefs) return defValue; + jclass prefsClass = env->GetObjectClass(prefs); + if (prefsClass) { + jmethodID contains = env->GetMethodID(prefsClass, "contains", "(Ljava/lang/String;)Z"); + jmethodID getBool = env->GetMethodID(prefsClass, "getBoolean", "(Ljava/lang/String;Z)Z"); + if (contains && getBool) { + jstring jkey = env->NewStringUTF(key); + if (jkey) { + jboolean hasKey = env->CallBooleanMethod(prefs, contains, jkey); + if (!HasException(env, "contains") && hasKey) { + out = (bool)env->CallBooleanMethod(prefs, getBool, jkey, (jboolean)defValue); + HasException(env, "getBoolean"); + } + env->DeleteLocalRef(jkey); + } + } + env->DeleteLocalRef(prefsClass); + } + env->DeleteLocalRef(prefs); + return out; +} + +static int GetPrefInt(JNIEnv* env, jobject activity, const char* key, int defValue) { + if (!env || !activity || !key || key[0] == '\0') { + return defValue; + } + int out = defValue; + jclass activityClass = env->GetObjectClass(activity); + if (!activityClass) return defValue; + jmethodID getPrefs = env->GetMethodID(activityClass, "getSharedPreferences", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences;"); + env->DeleteLocalRef(activityClass); + if (!getPrefs) return defValue; + jstring prefsName = env->NewStringUTF(kPrefsName); + if (!prefsName) return defValue; + jobject prefs = env->CallObjectMethod(activity, getPrefs, prefsName, 0); + env->DeleteLocalRef(prefsName); + if (HasException(env, "getSharedPreferences") || !prefs) return defValue; + jclass prefsClass = env->GetObjectClass(prefs); + if (prefsClass) { + jmethodID contains = env->GetMethodID(prefsClass, "contains", "(Ljava/lang/String;)Z"); + jmethodID getInt = env->GetMethodID(prefsClass, "getInt", "(Ljava/lang/String;I)I"); + if (contains && getInt) { + jstring jkey = env->NewStringUTF(key); + if (jkey) { + jboolean hasKey = env->CallBooleanMethod(prefs, contains, jkey); + if (!HasException(env, "contains") && hasKey) { + out = (int)env->CallIntMethod(prefs, getInt, jkey, (jint)defValue); + HasException(env, "getInt"); + } + env->DeleteLocalRef(jkey); + } + } + env->DeleteLocalRef(prefsClass); + } + env->DeleteLocalRef(prefs); + return out; +} + static bool CopyUriToPath(JNIEnv* env, jobject activity, const std::string& uriString, const std::string& path) { if (!env || !activity || uriString.empty() || path.empty()) return false; @@ -488,6 +562,15 @@ cleanup: return success; } +struct EmulatorSettings { + int surface_scale = 1; // 1, 2, or 3 + std::string tcg_thread = "multi"; // "single" or "multi" + bool use_dsp = true; + bool hrtf = true; + bool cache_shaders = true; + bool hard_fpu = true; +}; + struct SetupFiles { std::string mcpx; std::string flash; @@ -503,7 +586,8 @@ static bool WriteConfigToml(const std::string& config_path, const std::string& flash, const std::string& hdd, const std::string& dvd, - const std::string& eeprom) { + const std::string& eeprom, + const EmulatorSettings& settings) { if (config_path.empty()) return false; toml::table tbl; @@ -527,14 +611,16 @@ static bool WriteConfigToml(const std::string& config_path, toml::table* general = EnsureTable(tbl, "general"); toml::table* display = EnsureTable(tbl, "display"); + toml::table* display_quality = EnsureTable(*display, "quality"); toml::table* display_window = EnsureTable(*display, "window"); toml::table* audio = EnsureTable(tbl, "audio"); toml::table* audio_vp = EnsureTable(*audio, "vp"); toml::table* android = EnsureTable(tbl, "android"); toml::table* sys = EnsureTable(tbl, "sys"); + toml::table* perf = EnsureTable(tbl, "perf"); toml::table* files = EnsureTable(*sys, "files"); - if (!general || !display || !display_window || !audio || !audio_vp || - !android || !sys || !files) { + if (!general || !display || !display_quality || !display_window || !audio || + !audio_vp || !android || !sys || !perf || !files) { LogErrorFmt("Failed to build config tables at %s", config_path.c_str()); return false; } @@ -547,12 +633,22 @@ static bool WriteConfigToml(const std::string& config_path, if (!display_window->contains("vsync")) { display_window->insert_or_assign("vsync", false); } + // User-controlled settings (always written so they take effect immediately) + { + int scale = settings.surface_scale; + if (scale < 1) scale = 1; + if (scale > 3) scale = 3; + display_quality->insert_or_assign("surface_scale", scale); + } + audio->insert_or_assign("use_dsp", settings.use_dsp); + audio->insert_or_assign("hrtf", settings.hrtf); + perf->insert_or_assign("cache_shaders", settings.cache_shaders); + perf->insert_or_assign("hard_fpu", settings.hard_fpu); + android->insert_or_assign("tcg_thread", + (settings.tcg_thread == "single") ? "single" : "multi"); if (!audio_vp->contains("num_workers")) { audio_vp->insert_or_assign("num_workers", 0); } - if (!audio->contains("hrtf")) { - audio->insert_or_assign("hrtf", true); - } if (!audio->contains("volume_limit")) { audio->insert_or_assign("volume_limit", 1.0); } @@ -562,9 +658,6 @@ static bool WriteConfigToml(const std::string& config_path, if (!android->contains("tcg_tuning")) { android->insert_or_assign("tcg_tuning", true); } - if (!android->contains("tcg_thread")) { - android->insert_or_assign("tcg_thread", "multi"); - } if (!android->contains("tcg_tb_size")) { android->insert_or_assign("tcg_tb_size", 128); } @@ -681,8 +774,21 @@ static SetupFiles SyncSetupFiles() { } } + EmulatorSettings emuSettings; + emuSettings.surface_scale = GetPrefInt(env, activity, "setting_surface_scale", 1); + emuSettings.use_dsp = GetPrefBool(env, activity, "setting_use_dsp", true); + emuSettings.hrtf = GetPrefBool(env, activity, "setting_hrtf", true); + emuSettings.cache_shaders = GetPrefBool(env, activity, "setting_cache_shaders", true); + emuSettings.hard_fpu = GetPrefBool(env, activity, "setting_hard_fpu", true); + { + std::string tcgThread = GetPrefString(env, activity, "setting_tcg_thread"); + if (tcgThread == "single") { + emuSettings.tcg_thread = "single"; + } + } + out.config_path = base + "/xemu.toml"; - WriteConfigToml(out.config_path, out.mcpx, out.flash, out.hdd, out.dvd, out.eeprom); + WriteConfigToml(out.config_path, out.mcpx, out.flash, out.hdd, out.dvd, out.eeprom, emuSettings); LogInfoFmt("SyncSetupFiles: config %s", out.config_path.c_str()); LogInfoFmt("Resolved mcpx=%s", out.mcpx.c_str()); LogInfoFmt("Resolved flash=%s", out.flash.c_str()); diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index 17f7bcac7e..842195d457 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -229,6 +229,7 @@ bool xemu_settings_load(void) // Cache table lookups to avoid repeated traversal auto general = tbl["general"]; auto display = tbl["display"]; + auto display_quality = display["quality"]; auto display_window = display["window"]; auto audio = tbl["audio"]; auto audio_vp = audio["vp"]; @@ -253,6 +254,13 @@ bool xemu_settings_load(void) } } + if (auto surface_scale = display_quality["surface_scale"].value()) { + int scale = (int)*surface_scale; + if (scale < 1) scale = 1; + if (scale > 3) scale = 3; + g_config.display.quality.surface_scale = scale; + } + if (auto filtering = display["filtering"].value()) { CONFIG_DISPLAY_FILTERING parsed; if (parse_filtering(*filtering, &parsed)) { diff --git a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt index 9bbf250434..1513d2c370 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt @@ -71,6 +71,7 @@ class GameLibraryActivity : AppCompatActivity() { private lateinit var gamesListContainer: LinearLayout private lateinit var gamesGridContainer: LinearLayout private lateinit var btnChangeFolder: MaterialButton + private lateinit var btnSettings: MaterialButton private lateinit var btnConvertIso: MaterialButton private lateinit var btnAbout: ImageButton private lateinit var viewModeToggle: MaterialButtonToggleGroup @@ -110,6 +111,7 @@ class GameLibraryActivity : AppCompatActivity() { gamesListContainer = findViewById(R.id.library_games_container) gamesGridContainer = findViewById(R.id.library_games_grid_container) btnChangeFolder = findViewById(R.id.btn_change_games_folder) + btnSettings = findViewById(R.id.btn_settings) btnConvertIso = findViewById(R.id.btn_convert_iso) btnAbout = findViewById(R.id.btn_library_about) viewModeToggle = findViewById(R.id.library_view_mode_toggle) @@ -130,6 +132,9 @@ class GameLibraryActivity : AppCompatActivity() { } pickGamesFolder.launch(gamesFolderUri) } + btnSettings.setOnClickListener { + startActivity(Intent(this, SettingsActivity::class.java)) + } btnConvertIso.setOnClickListener { showIsoConversionPicker() } diff --git a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt new file mode 100644 index 0000000000..d97ae7b345 --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt @@ -0,0 +1,76 @@ +package com.izzy2lost.x1box + +import android.content.Context +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.materialswitch.MaterialSwitch + +class SettingsActivity : AppCompatActivity() { + + private val prefs by lazy { getSharedPreferences("x1box_prefs", Context.MODE_PRIVATE) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + val toggleScale = findViewById(R.id.toggle_resolution_scale) + val btn1x = findViewById(R.id.btn_scale_1x) + val btn2x = findViewById(R.id.btn_scale_2x) + val btn3x = findViewById(R.id.btn_scale_3x) + val toggleThread = findViewById(R.id.toggle_tcg_thread) + val btnMulti = findViewById(R.id.btn_thread_multi) + val btnSingle = findViewById(R.id.btn_thread_single) + val switchDsp = findViewById(R.id.switch_use_dsp) + val switchHrtf = findViewById(R.id.switch_hrtf) + val switchShaders= findViewById(R.id.switch_cache_shaders) + val switchFpu = findViewById(R.id.switch_hard_fpu) + val btnSave = findViewById(R.id.btn_settings_save) + + // Load current values + val scale = prefs.getInt("setting_surface_scale", 1) + when (scale) { + 2 -> toggleScale.check(R.id.btn_scale_2x) + 3 -> toggleScale.check(R.id.btn_scale_3x) + else -> toggleScale.check(R.id.btn_scale_1x) + } + + val tcgThread = prefs.getString("setting_tcg_thread", "multi") ?: "multi" + if (tcgThread == "single") { + toggleThread.check(R.id.btn_thread_single) + } else { + toggleThread.check(R.id.btn_thread_multi) + } + + switchDsp.isChecked = prefs.getBoolean("setting_use_dsp", true) + switchHrtf.isChecked = prefs.getBoolean("setting_hrtf", true) + switchShaders.isChecked = prefs.getBoolean("setting_cache_shaders", true) + switchFpu.isChecked = prefs.getBoolean("setting_hard_fpu", true) + + btnSave.setOnClickListener { + val selectedScale = when (toggleScale.checkedButtonId) { + R.id.btn_scale_2x -> 2 + R.id.btn_scale_3x -> 3 + else -> 1 + } + val selectedThread = when (toggleThread.checkedButtonId) { + R.id.btn_thread_single -> "single" + else -> "multi" + } + + prefs.edit() + .putInt("setting_surface_scale", selectedScale) + .putString("setting_tcg_thread", selectedThread) + .putBoolean("setting_use_dsp", switchDsp.isChecked) + .putBoolean("setting_hrtf", switchHrtf.isChecked) + .putBoolean("setting_cache_shaders", switchShaders.isChecked) + .putBoolean("setting_hard_fpu", switchFpu.isChecked) + .apply() + + Toast.makeText(this, R.string.settings_saved, Toast.LENGTH_SHORT).show() + finish() + } + } +} diff --git a/android/app/src/main/res/layout/activity_game_library.xml b/android/app/src/main/res/layout/activity_game_library.xml index 78ae231c81..8f560f12ed 100644 --- a/android/app/src/main/res/layout/activity_game_library.xml +++ b/android/app/src/main/res/layout/activity_game_library.xml @@ -86,6 +86,17 @@ app:iconGravity="textStart" app:iconPadding="8dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5a2e01cf05..fdf610390b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -73,6 +73,30 @@ https://github.com/izzy2lost/xemu/blob/master/LICENSE https://github.com/izzy2lost/xemu/blob/master/COPYING + Settings + + Settings + Save + + Display + Internal Resolution Scale + 1x (Native 480p) + 2x (960p) + 3x (1440p) + + Performance + CPU Threading Mode + Multi-thread + Single-thread + Hardware FPU + Shader Cache + + Audio + Real-time DSP Processing + HRTF (3D Audio) + + Settings saved + Game Menu Resume Restart Game