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