add settings options

This commit is contained in:
izzy2lost
2026-02-23 17:55:14 -05:00
parent c4c4eae20f
commit aa7de6dc2b
8 changed files with 442 additions and 10 deletions
+5
View File
@@ -43,5 +43,10 @@
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".SettingsActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
</application>
</manifest>
+116 -10
View File
@@ -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());
@@ -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<int64_t>()) {
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<std::string>()) {
CONFIG_DISPLAY_FILTERING parsed;
if (parse_filtering(*filtering, &parsed)) {
@@ -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()
}
@@ -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<MaterialButtonToggleGroup>(R.id.toggle_resolution_scale)
val btn1x = findViewById<MaterialButton>(R.id.btn_scale_1x)
val btn2x = findViewById<MaterialButton>(R.id.btn_scale_2x)
val btn3x = findViewById<MaterialButton>(R.id.btn_scale_3x)
val toggleThread = findViewById<MaterialButtonToggleGroup>(R.id.toggle_tcg_thread)
val btnMulti = findViewById<MaterialButton>(R.id.btn_thread_multi)
val btnSingle = findViewById<MaterialButton>(R.id.btn_thread_single)
val switchDsp = findViewById<MaterialSwitch>(R.id.switch_use_dsp)
val switchHrtf = findViewById<MaterialSwitch>(R.id.switch_hrtf)
val switchShaders= findViewById<MaterialSwitch>(R.id.switch_cache_shaders)
val switchFpu = findViewById<MaterialSwitch>(R.id.switch_hard_fpu)
val btnSave = findViewById<MaterialButton>(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()
}
}
}
@@ -86,6 +86,17 @@
app:iconGravity="textStart"
app:iconPadding="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_settings"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/library_settings"
app:icon="@android:drawable/ic_menu_preferences"
app:iconGravity="textStart"
app:iconPadding="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_convert_iso"
style="?attr/materialButtonOutlinedStyle"
@@ -0,0 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/setup_wizard_background"
android:fitsSystemWindows="true">
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:fillViewport="true"
android:padding="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/xemu_surface"
app:cardCornerRadius="28dp"
app:cardElevation="8dp"
app:strokeColor="@color/xemu_outline_variant"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<!-- Title -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/settings_title"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
<!-- ── DISPLAY ── -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/settings_section_display"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_resolution_scale"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggle_resolution_scale"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_scale_1x"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_resolution_scale_1x" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_scale_2x"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_resolution_scale_2x" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_scale_3x"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_resolution_scale_3x" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<!-- ── PERFORMANCE ── -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="@string/settings_section_performance"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_tcg_thread"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggle_tcg_thread"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_thread_multi"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_tcg_thread_multi" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_thread_single"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/settings_tcg_thread_single" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_hard_fpu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/settings_hard_fpu"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_cache_shaders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/settings_cache_shaders"
android:textColor="@color/xemu_text_muted" />
<!-- ── AUDIO ── -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/settings_section_audio"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_use_dsp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_use_dsp"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_hrtf"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/settings_hrtf"
android:textColor="@color/xemu_text_muted" />
<!-- Save button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_settings_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/settings_save" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -73,6 +73,30 @@
<string name="library_about_url_license" translatable="false">https://github.com/izzy2lost/xemu/blob/master/LICENSE</string>
<string name="library_about_url_disclaimer" translatable="false">https://github.com/izzy2lost/xemu/blob/master/COPYING</string>
<string name="library_settings">Settings</string>
<string name="settings_title">Settings</string>
<string name="settings_save">Save</string>
<string name="settings_section_display">Display</string>
<string name="settings_resolution_scale">Internal Resolution Scale</string>
<string name="settings_resolution_scale_1x">1x (Native 480p)</string>
<string name="settings_resolution_scale_2x">2x (960p)</string>
<string name="settings_resolution_scale_3x">3x (1440p)</string>
<string name="settings_section_performance">Performance</string>
<string name="settings_tcg_thread">CPU Threading Mode</string>
<string name="settings_tcg_thread_multi">Multi-thread</string>
<string name="settings_tcg_thread_single">Single-thread</string>
<string name="settings_hard_fpu">Hardware FPU</string>
<string name="settings_cache_shaders">Shader Cache</string>
<string name="settings_section_audio">Audio</string>
<string name="settings_use_dsp">Real-time DSP Processing</string>
<string name="settings_hrtf">HRTF (3D Audio)</string>
<string name="settings_saved">Settings saved</string>
<string name="in_game_menu_title">Game Menu</string>
<string name="in_game_menu_resume">Resume</string>
<string name="in_game_menu_restart_game">Restart Game</string>