From 51e16997ff7785f467c3a2b025b1a03971224fb3 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Fri, 13 Mar 2026 10:00:39 -0400 Subject: [PATCH] more settings options --- .../app/src/main/cpp/xemu_snapshots_stub.c | 15 ++ .../java/com/izzy2lost/x1box/MainActivity.kt | 19 +- .../com/izzy2lost/x1box/SettingsActivity.kt | 244 +++++++++++++++++- .../com/izzy2lost/x1box/XboxEepromEditor.kt | 104 +++++++- .../src/main/res/layout/activity_settings.xml | 78 ++++++ android/app/src/main/res/values/strings.xml | 25 +- hw/xbox/mcpx/apu/vp/vp.c | 3 +- 7 files changed, 478 insertions(+), 10 deletions(-) diff --git a/android/app/src/main/cpp/xemu_snapshots_stub.c b/android/app/src/main/cpp/xemu_snapshots_stub.c index 3134858617..7392022916 100644 --- a/android/app/src/main/cpp/xemu_snapshots_stub.c +++ b/android/app/src/main/cpp/xemu_snapshots_stub.c @@ -23,6 +23,7 @@ static bool xemu_snapshots_dirty = true; static GLuint g_snapshot_display_tex = 0; static bool g_snapshot_display_flip = false; static SDL_atomic_t g_snapshot_pending = { 0 }; +static SDL_atomic_t g_reboot_pending = { 0 }; #define SNAPSHOT_PREVIEW_WIDTH 320 #define SNAPSHOT_PREVIEW_HEIGHT 240 @@ -391,6 +392,11 @@ static struct { void xemu_android_process_snapshot_request(void) { + if (SDL_AtomicCAS(&g_reboot_pending, 1, 0)) { + qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET); + return; + } + if (SDL_AtomicGet(&g_snapshot_pending) == 0) { return; } @@ -469,3 +475,12 @@ Java_com_izzy2lost_x1box_MainActivity_nativeLoadSnapshot( (void)obj; return dispatch_snapshot(env, name, SNAP_LOAD); } + +JNIEXPORT void JNICALL +Java_com_izzy2lost_x1box_MainActivity_nativeRebootSystem( + JNIEnv *env, jobject obj) +{ + (void)env; + (void)obj; + SDL_AtomicSet(&g_reboot_pending, 1); +} diff --git a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt index df18e03126..bc781d4286 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt @@ -434,6 +434,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { private external fun nativeSaveSnapshot(name: String): Boolean private external fun nativeLoadSnapshot(name: String): Boolean + private external fun nativeRebootSystem() private fun slotName(slot: Int) = "android_slot_$slot" @@ -699,6 +700,18 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { showSnapshotSlotDialog(save = false) } + private fun showRebootSystemConfirmation() { + MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Xemu_RoundedDialog) + .setTitle(R.string.in_game_menu_reboot_title) + .setMessage(R.string.in_game_menu_reboot_message) + .setPositiveButton(R.string.in_game_menu_reboot_action) { _, _ -> + onScreenController?.resetAllInputs() + nativeRebootSystem() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + private fun showInGameMenu() { swipeUpGestureRecognizer.reset() if (inGameMenuDialog?.isShowing == true) { @@ -714,6 +727,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { }, getString(R.string.in_game_menu_save_state), getString(R.string.in_game_menu_load_state), + getString(R.string.in_game_menu_reboot_system), getString(R.string.in_game_menu_exit_to_library), getString(R.string.in_game_menu_quit_app), ) @@ -726,8 +740,9 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { 1 -> toggleOnScreenController() 2 -> showSaveStateDialog() 3 -> showLoadStateDialog() - 4 -> exitToGameLibrary() - 5 -> quitApp() + 4 -> showRebootSystemConfirmation() + 5 -> exitToGameLibrary() + 6 -> quitApp() } } .setOnDismissListener { diff --git a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt index b228d2189e..8ae2cd3c6c 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt @@ -13,6 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.textfield.TextInputLayout import java.io.File @@ -31,6 +32,21 @@ class SettingsActivity : AppCompatActivity() { val labelRes: Int, ) + private data class EepromAspectRatioOption( + val value: XboxEepromEditor.AspectRatio, + val labelRes: Int, + ) + + private data class EepromRefreshRateOption( + val value: XboxEepromEditor.RefreshRate, + val labelRes: Int, + ) + + private data class CacheClearResult( + val deletedEntries: Int, + val hadFailures: Boolean, + ) + private val eepromLanguageOptions = listOf( EepromLanguageOption(XboxEepromEditor.Language.ENGLISH, R.string.settings_eeprom_language_english), EepromLanguageOption(XboxEepromEditor.Language.JAPANESE, R.string.settings_eeprom_language_japanese), @@ -50,6 +66,18 @@ class SettingsActivity : AppCompatActivity() { EepromVideoOption(XboxEepromEditor.VideoStandard.PAL_M, R.string.settings_eeprom_video_standard_pal_m), ) + private val eepromAspectRatioOptions = listOf( + EepromAspectRatioOption(XboxEepromEditor.AspectRatio.NORMAL, R.string.settings_eeprom_aspect_ratio_normal), + EepromAspectRatioOption(XboxEepromEditor.AspectRatio.WIDESCREEN, R.string.settings_eeprom_aspect_ratio_widescreen), + EepromAspectRatioOption(XboxEepromEditor.AspectRatio.LETTERBOX, R.string.settings_eeprom_aspect_ratio_letterbox), + ) + + private val eepromRefreshRateOptions = listOf( + EepromRefreshRateOption(XboxEepromEditor.RefreshRate.DEFAULT, R.string.settings_eeprom_refresh_rate_default), + EepromRefreshRateOption(XboxEepromEditor.RefreshRate.HZ_60, R.string.settings_eeprom_refresh_rate_60), + EepromRefreshRateOption(XboxEepromEditor.RefreshRate.HZ_50, R.string.settings_eeprom_refresh_rate_50), + ) + private var pendingVulkanUri: String? = null private var pendingVulkanName: String? = null private var clearVulkan = false @@ -58,11 +86,20 @@ class SettingsActivity : AppCompatActivity() { private lateinit var tvEepromStatus: TextView private lateinit var inputEepromLanguage: TextInputLayout private lateinit var inputEepromVideoStandard: TextInputLayout + private lateinit var inputEepromAspectRatio: TextInputLayout + private lateinit var inputEepromRefreshRate: TextInputLayout private lateinit var dropdownEepromLanguage: AutoCompleteTextView private lateinit var dropdownEepromVideoStandard: AutoCompleteTextView + private lateinit var dropdownEepromAspectRatio: AutoCompleteTextView + private lateinit var dropdownEepromRefreshRate: AutoCompleteTextView + private lateinit var switchEeprom480p: MaterialSwitch + private lateinit var switchEeprom720p: MaterialSwitch + private lateinit var switchEeprom1080i: MaterialSwitch private var selectedEepromLanguage = XboxEepromEditor.Language.ENGLISH private var selectedEepromVideoStandard = XboxEepromEditor.VideoStandard.NTSC_M + private var selectedEepromAspectRatio = XboxEepromEditor.AspectRatio.NORMAL + private var selectedEepromRefreshRate = XboxEepromEditor.RefreshRate.DEFAULT private var eepromEditable = false private var eepromMissing = false private var eepromError = false @@ -103,14 +140,22 @@ class SettingsActivity : AppCompatActivity() { val toggleAudioDriver = findViewById(R.id.toggle_audio_driver) val btnSave = findViewById(R.id.btn_settings_save) val btnRedoSetup = findViewById(R.id.btn_redo_setup_wizard) + val btnClearCache = findViewById(R.id.btn_clear_system_cache) tvVulkanDriverName = findViewById(R.id.tv_vulkan_driver_name) val btnVulkanBrowse = findViewById(R.id.btn_vulkan_browse) val btnVulkanClear = findViewById(R.id.btn_vulkan_clear) tvEepromStatus = findViewById(R.id.tv_eeprom_status) inputEepromLanguage = findViewById(R.id.input_eeprom_language) inputEepromVideoStandard = findViewById(R.id.input_eeprom_video_standard) + inputEepromAspectRatio = findViewById(R.id.input_eeprom_aspect_ratio) + inputEepromRefreshRate = findViewById(R.id.input_eeprom_refresh_rate) dropdownEepromLanguage = findViewById(R.id.dropdown_eeprom_language) dropdownEepromVideoStandard = findViewById(R.id.dropdown_eeprom_video_standard) + dropdownEepromAspectRatio = findViewById(R.id.dropdown_eeprom_aspect_ratio) + dropdownEepromRefreshRate = findViewById(R.id.dropdown_eeprom_refresh_rate) + switchEeprom480p = findViewById(R.id.switch_eeprom_480p) + switchEeprom720p = findViewById(R.id.switch_eeprom_720p) + switchEeprom1080i = findViewById(R.id.switch_eeprom_1080i) // Load current values val renderer = prefs.getString("setting_renderer", "opengl") ?: "opengl" @@ -198,7 +243,7 @@ class SettingsActivity : AppCompatActivity() { setupEepromEditor() - btnSave.setOnClickListener { + fun persistSettings(): Pair { val selectedDisplayMode = when (toggleDisplayMode.checkedButtonId) { R.id.btn_display_4_3 -> 1 R.id.btn_display_16_9 -> 2 @@ -263,7 +308,15 @@ class SettingsActivity : AppCompatActivity() { edit.apply() - val toastResult = applyEepromEdits() + return applyEepromEdits() + } + + btnClearCache.setOnClickListener { + showClearCacheConfirmation() + } + + btnSave.setOnClickListener { + val toastResult = persistSettings() Toast.makeText(this, toastResult.first, toastResult.second).show() finish() } @@ -272,6 +325,8 @@ class SettingsActivity : AppCompatActivity() { private fun setupEepromEditor() { val languageLabels = eepromLanguageOptions.map { getString(it.labelRes) } val videoLabels = eepromVideoOptions.map { getString(it.labelRes) } + val aspectRatioLabels = eepromAspectRatioOptions.map { getString(it.labelRes) } + val refreshRateLabels = eepromRefreshRateOptions.map { getString(it.labelRes) } dropdownEepromLanguage.setAdapter( ArrayAdapter(this, android.R.layout.simple_list_item_1, languageLabels) @@ -279,6 +334,12 @@ class SettingsActivity : AppCompatActivity() { dropdownEepromVideoStandard.setAdapter( ArrayAdapter(this, android.R.layout.simple_list_item_1, videoLabels) ) + dropdownEepromAspectRatio.setAdapter( + ArrayAdapter(this, android.R.layout.simple_list_item_1, aspectRatioLabels) + ) + dropdownEepromRefreshRate.setAdapter( + ArrayAdapter(this, android.R.layout.simple_list_item_1, refreshRateLabels) + ) dropdownEepromLanguage.setOnItemClickListener { _, _, position, _ -> selectedEepromLanguage = eepromLanguageOptions[position].value @@ -286,6 +347,12 @@ class SettingsActivity : AppCompatActivity() { dropdownEepromVideoStandard.setOnItemClickListener { _, _, position, _ -> selectedEepromVideoStandard = eepromVideoOptions[position].value } + dropdownEepromAspectRatio.setOnItemClickListener { _, _, position, _ -> + selectedEepromAspectRatio = eepromAspectRatioOptions[position].value + } + dropdownEepromRefreshRate.setOnItemClickListener { _, _, position, _ -> + selectedEepromRefreshRate = eepromRefreshRateOptions[position].value + } val eepromFile = resolveEepromFile() if (!eepromFile.isFile) { @@ -295,6 +362,15 @@ class SettingsActivity : AppCompatActivity() { setEepromEditorEnabled(false) setEepromLanguageSelection(selectedEepromLanguage) setEepromVideoSelection(selectedEepromVideoStandard) + setEepromVideoSettingsSelection( + XboxEepromEditor.VideoSettings( + allow480p = false, + allow720p = false, + allow1080i = false, + aspectRatio = selectedEepromAspectRatio, + refreshRate = selectedEepromRefreshRate, + ) + ) tvEepromStatus.text = getString( R.string.settings_eeprom_status_missing, eepromFile.absolutePath, @@ -310,10 +386,12 @@ class SettingsActivity : AppCompatActivity() { setEepromEditorEnabled(true) setEepromLanguageSelection(snapshot.language) setEepromVideoSelection(snapshot.videoStandard) + setEepromVideoSettingsSelection(snapshot.videoSettings) val hasUnknownValues = snapshot.rawLanguage != snapshot.language.id || - snapshot.rawVideoStandard != snapshot.videoStandard.id + snapshot.rawVideoStandard != snapshot.videoStandard.id || + snapshot.hasManagedVideoSettingsMismatch tvEepromStatus.text = if (hasUnknownValues) { getString(R.string.settings_eeprom_status_unknown, eepromFile.absolutePath) } else { @@ -326,6 +404,15 @@ class SettingsActivity : AppCompatActivity() { setEepromEditorEnabled(false) setEepromLanguageSelection(selectedEepromLanguage) setEepromVideoSelection(selectedEepromVideoStandard) + setEepromVideoSettingsSelection( + XboxEepromEditor.VideoSettings( + allow480p = false, + allow720p = false, + allow1080i = false, + aspectRatio = selectedEepromAspectRatio, + refreshRate = selectedEepromRefreshRate, + ) + ) tvEepromStatus.text = getString( R.string.settings_eeprom_status_invalid, eepromFile.absolutePath, @@ -337,6 +424,15 @@ class SettingsActivity : AppCompatActivity() { setEepromEditorEnabled(false) setEepromLanguageSelection(selectedEepromLanguage) setEepromVideoSelection(selectedEepromVideoStandard) + setEepromVideoSettingsSelection( + XboxEepromEditor.VideoSettings( + allow480p = false, + allow720p = false, + allow1080i = false, + aspectRatio = selectedEepromAspectRatio, + refreshRate = selectedEepromRefreshRate, + ) + ) tvEepromStatus.text = getString( R.string.settings_eeprom_status_error, eepromFile.absolutePath, @@ -357,6 +453,13 @@ class SettingsActivity : AppCompatActivity() { resolveEepromFile(), selectedEepromLanguage, selectedEepromVideoStandard, + XboxEepromEditor.VideoSettings( + allow480p = switchEeprom480p.isChecked, + allow720p = switchEeprom720p.isChecked, + allow1080i = switchEeprom1080i.isChecked, + aspectRatio = selectedEepromAspectRatio, + refreshRate = selectedEepromRefreshRate, + ), ) if (changed) { Pair(R.string.settings_saved_with_eeprom, Toast.LENGTH_SHORT) @@ -371,8 +474,15 @@ class SettingsActivity : AppCompatActivity() { private fun setEepromEditorEnabled(enabled: Boolean) { inputEepromLanguage.isEnabled = enabled inputEepromVideoStandard.isEnabled = enabled + inputEepromAspectRatio.isEnabled = enabled + inputEepromRefreshRate.isEnabled = enabled dropdownEepromLanguage.isEnabled = enabled dropdownEepromVideoStandard.isEnabled = enabled + dropdownEepromAspectRatio.isEnabled = enabled + dropdownEepromRefreshRate.isEnabled = enabled + switchEeprom480p.isEnabled = enabled + switchEeprom720p.isEnabled = enabled + switchEeprom1080i.isEnabled = enabled } private fun setEepromLanguageSelection(language: XboxEepromEditor.Language) { @@ -389,6 +499,134 @@ class SettingsActivity : AppCompatActivity() { dropdownEepromVideoStandard.setText(getString(option.labelRes), false) } + private fun setEepromVideoSettingsSelection(videoSettings: XboxEepromEditor.VideoSettings) { + switchEeprom480p.isChecked = videoSettings.allow480p + switchEeprom720p.isChecked = videoSettings.allow720p + switchEeprom1080i.isChecked = videoSettings.allow1080i + setEepromAspectRatioSelection(videoSettings.aspectRatio) + setEepromRefreshRateSelection(videoSettings.refreshRate) + } + + private fun setEepromAspectRatioSelection(aspectRatio: XboxEepromEditor.AspectRatio) { + selectedEepromAspectRatio = aspectRatio + val option = eepromAspectRatioOptions.firstOrNull { it.value == aspectRatio } + ?: eepromAspectRatioOptions.first() + dropdownEepromAspectRatio.setText(getString(option.labelRes), false) + } + + private fun setEepromRefreshRateSelection(refreshRate: XboxEepromEditor.RefreshRate) { + selectedEepromRefreshRate = refreshRate + val option = eepromRefreshRateOptions.firstOrNull { it.value == refreshRate } + ?: eepromRefreshRateOptions.first() + dropdownEepromRefreshRate.setText(getString(option.labelRes), false) + } + + private fun showClearCacheConfirmation() { + MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Xemu_RoundedDialog) + .setTitle(R.string.settings_clear_cache_title) + .setMessage(R.string.settings_clear_cache_message) + .setPositiveButton(R.string.settings_clear_cache_action) { _, _ -> + val result = clearSystemCache() + val messageRes = when { + result.hadFailures -> R.string.settings_clear_cache_partial + result.deletedEntries > 0 -> R.string.settings_clear_cache_success + else -> R.string.settings_clear_cache_empty + } + Toast.makeText(this, getString(messageRes), Toast.LENGTH_SHORT).show() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun clearSystemCache(): CacheClearResult { + var result = CacheClearResult(0, false) + + val cacheRoots = buildList { + add(cacheDir) + add(codeCacheDir) + externalCacheDir?.let { add(it) } + } + for (root in cacheRoots.distinctBy { it.absolutePath }) { + result = mergeCacheClearResults(result, clearDirectoryChildren(root)) + } + + val persistentRoots = buildList { + add(filesDir) + getExternalFilesDir(null)?.let { add(it) } + } + for (root in persistentRoots.distinctBy { it.absolutePath }) { + result = mergeCacheClearResults(result, clearPersistentCacheEntries(root)) + } + + return result + } + + private fun clearDirectoryChildren(dir: File?): CacheClearResult { + if (dir == null || !dir.exists()) { + return CacheClearResult(0, false) + } + + val children = dir.listFiles() ?: return CacheClearResult(0, false) + var deletedEntries = 0 + var hadFailures = false + for (child in children) { + val deleted = runCatching { child.deleteRecursively() }.getOrDefault(false) + if (deleted) { + deletedEntries++ + } else { + hadFailures = true + } + } + return CacheClearResult(deletedEntries, hadFailures) + } + + private fun clearPersistentCacheEntries(root: File): CacheClearResult { + if (!root.exists() || !root.isDirectory) { + return CacheClearResult(0, false) + } + + val children = root.listFiles() ?: return CacheClearResult(0, false) + var deletedEntries = 0 + var hadFailures = false + + for (child in children) { + if (isPersistentCacheEntry(child.name)) { + val deleted = runCatching { child.deleteRecursively() }.getOrDefault(false) + if (deleted) { + deletedEntries++ + } else { + hadFailures = true + } + continue + } + + if (child.isDirectory) { + val nested = clearPersistentCacheEntries(child) + deletedEntries += nested.deletedEntries + hadFailures = hadFailures || nested.hadFailures + } + } + + return CacheClearResult(deletedEntries, hadFailures) + } + + private fun isPersistentCacheEntry(name: String): Boolean { + return name == "shaders" || + name == "shader_cache_list" || + name.startsWith("scache-") || + name.startsWith("vk_pipeline_cache_") + } + + private fun mergeCacheClearResults( + first: CacheClearResult, + second: CacheClearResult, + ): CacheClearResult { + return CacheClearResult( + deletedEntries = first.deletedEntries + second.deletedEntries, + hadFailures = first.hadFailures || second.hadFailures, + ) + } + private fun resolveEepromFile(): File { val base = getExternalFilesDir(null) ?: filesDir return File(File(base, "x1box"), "eeprom.bin") diff --git a/android/app/src/main/java/com/izzy2lost/x1box/XboxEepromEditor.kt b/android/app/src/main/java/com/izzy2lost/x1box/XboxEepromEditor.kt index 4feef67224..a3263ae63a 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/XboxEepromEditor.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/XboxEepromEditor.kt @@ -16,6 +16,24 @@ internal object XboxEepromEditor { private const val VIDEO_STANDARD_OFFSET = 0x58 private const val LANGUAGE_OFFSET = 0x90 + private const val VIDEO_SETTINGS_OFFSET = 0x94 + + private const val VIDEO_SETTINGS_WIDESCREEN = 0x00010000u + private const val VIDEO_SETTINGS_720P = 0x00020000u + private const val VIDEO_SETTINGS_1080I = 0x00040000u + private const val VIDEO_SETTINGS_480P = 0x00080000u + private const val VIDEO_SETTINGS_LETTERBOX = 0x00100000u + private const val VIDEO_SETTINGS_60HZ = 0x00400000u + private const val VIDEO_SETTINGS_50HZ = 0x00800000u + + private val MANAGED_VIDEO_SETTINGS_MASK = + VIDEO_SETTINGS_WIDESCREEN or + VIDEO_SETTINGS_720P or + VIDEO_SETTINGS_1080I or + VIDEO_SETTINGS_480P or + VIDEO_SETTINGS_LETTERBOX or + VIDEO_SETTINGS_60HZ or + VIDEO_SETTINGS_50HZ enum class Language(val id: UInt) { ENGLISH(0x00000001u), @@ -44,12 +62,71 @@ internal object XboxEepromEditor { } } + enum class AspectRatio(val bits: UInt) { + NORMAL(0u), + WIDESCREEN(VIDEO_SETTINGS_WIDESCREEN), + LETTERBOX(VIDEO_SETTINGS_LETTERBOX); + + companion object { + fun fromBits(bits: UInt): AspectRatio? = when (bits and + (VIDEO_SETTINGS_WIDESCREEN or VIDEO_SETTINGS_LETTERBOX)) { + 0u -> NORMAL + VIDEO_SETTINGS_WIDESCREEN -> WIDESCREEN + VIDEO_SETTINGS_LETTERBOX -> LETTERBOX + else -> null + } + } + } + + enum class RefreshRate(val bits: UInt) { + DEFAULT(0u), + HZ_60(VIDEO_SETTINGS_60HZ), + HZ_50(VIDEO_SETTINGS_50HZ); + + companion object { + fun fromBits(bits: UInt): RefreshRate? = when (bits and + (VIDEO_SETTINGS_60HZ or VIDEO_SETTINGS_50HZ)) { + 0u -> DEFAULT + VIDEO_SETTINGS_60HZ -> HZ_60 + VIDEO_SETTINGS_50HZ -> HZ_50 + else -> null + } + } + } + + data class VideoSettings( + val allow480p: Boolean, + val allow720p: Boolean, + val allow1080i: Boolean, + val aspectRatio: AspectRatio, + val refreshRate: RefreshRate, + ) { + fun toManagedBits(): UInt { + var bits = aspectRatio.bits or refreshRate.bits + if (allow480p) { + bits = bits or VIDEO_SETTINGS_480P + } + if (allow720p) { + bits = bits or VIDEO_SETTINGS_720P + } + if (allow1080i) { + bits = bits or VIDEO_SETTINGS_1080I + } + return bits + } + } + data class Snapshot( val language: Language, val videoStandard: VideoStandard, + val videoSettings: VideoSettings, val rawLanguage: UInt, val rawVideoStandard: UInt, - ) + val rawVideoSettings: UInt, + ) { + val hasManagedVideoSettingsMismatch: Boolean + get() = (rawVideoSettings and MANAGED_VIDEO_SETTINGS_MASK) != videoSettings.toManagedBits() + } @Throws(IOException::class, IllegalArgumentException::class) fun load(file: File): Snapshot { @@ -58,28 +135,49 @@ internal object XboxEepromEditor { val rawLanguage = readLeUInt32(data, LANGUAGE_OFFSET) val rawVideoStandard = readLeUInt32(data, VIDEO_STANDARD_OFFSET) + val rawVideoSettings = readLeUInt32(data, VIDEO_SETTINGS_OFFSET) return Snapshot( language = Language.fromId(rawLanguage) ?: Language.ENGLISH, videoStandard = VideoStandard.fromId(rawVideoStandard) ?: VideoStandard.NTSC_M, + videoSettings = VideoSettings( + allow480p = (rawVideoSettings and VIDEO_SETTINGS_480P) != 0u, + allow720p = (rawVideoSettings and VIDEO_SETTINGS_720P) != 0u, + allow1080i = (rawVideoSettings and VIDEO_SETTINGS_1080I) != 0u, + aspectRatio = AspectRatio.fromBits(rawVideoSettings) ?: AspectRatio.NORMAL, + refreshRate = RefreshRate.fromBits(rawVideoSettings) ?: RefreshRate.DEFAULT, + ), rawLanguage = rawLanguage, rawVideoStandard = rawVideoStandard, + rawVideoSettings = rawVideoSettings, ) } @Throws(IOException::class, IllegalArgumentException::class) - fun apply(file: File, language: Language, videoStandard: VideoStandard): Boolean { + fun apply( + file: File, + language: Language, + videoStandard: VideoStandard, + videoSettings: VideoSettings, + ): Boolean { val data = file.readBytes() ensureValidSize(file, data) val currentLanguage = readLeUInt32(data, LANGUAGE_OFFSET) val currentVideoStandard = readLeUInt32(data, VIDEO_STANDARD_OFFSET) - if (currentLanguage == language.id && currentVideoStandard == videoStandard.id) { + val currentVideoSettings = readLeUInt32(data, VIDEO_SETTINGS_OFFSET) + val nextVideoSettings = + (currentVideoSettings and MANAGED_VIDEO_SETTINGS_MASK.inv()) or videoSettings.toManagedBits() + if (currentLanguage == language.id && + currentVideoStandard == videoStandard.id && + currentVideoSettings == nextVideoSettings + ) { return false } writeLeUInt32(data, LANGUAGE_OFFSET, language.id) writeLeUInt32(data, VIDEO_STANDARD_OFFSET, videoStandard.id) + writeLeUInt32(data, VIDEO_SETTINGS_OFFSET, nextVideoSettings) val factoryChecksum = xboxChecksum(data, FACTORY_CHECKSUM_START, FACTORY_CHECKSUM_LENGTH) writeLeUInt32(data, FACTORY_CHECKSUM_OFFSET, factoryChecksum) diff --git a/android/app/src/main/res/layout/activity_settings.xml b/android/app/src/main/res/layout/activity_settings.xml index ec8a021ed9..932d2a0767 100644 --- a/android/app/src/main/res/layout/activity_settings.xml +++ b/android/app/src/main/res/layout/activity_settings.xml @@ -519,6 +519,7 @@ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="10dp" android:hint="@string/settings_eeprom_video_standard"> + + + + + + + + + + + + + + + + + + + + + + + + NTSC-J PAL-I PAL-M + EEPROM Resolution Modes + Enable 480p + Enable 720p + Enable 1080i + EEPROM Aspect Ratio + Normal (4:3) + Widescreen (16:9) + Letterbox + EEPROM Refresh Rate + Default / Unset + 60 Hz + 50 Hz EEPROM file: %1$s EEPROM not found at %1$s. Launch a game once to create it. EEPROM is invalid at %1$s. Failed to read EEPROM at %1$s. EEPROM file: %1$s (contains unknown values; saving will normalize them) - Updates language and video standard in eeprom.bin. Restart emulation after saving. + Updates language and Xbox video settings in eeprom.bin. HD modes depend on the game, cable, and video standard. Restart emulation after saving. Settings saved Settings and EEPROM saved Settings saved. Launch a game once, then edit EEPROM. Settings saved, but EEPROM update failed + Maintenance + Clear Cache + Clear emulator cache files now? This removes shader and app cache data, but keeps your games, saves, and HDD image. + Clear Cache + Cache cleared + Some cache files could not be removed + Cache is already clear Game Menu Resume Restart Game Show Touch Controls Hide Touch Controls + Reboot System + Reboot System + Are you sure you want to reboot the emulated Xbox system? + Reboot Exit to Game Library Quit App Save State diff --git a/hw/xbox/mcpx/apu/vp/vp.c b/hw/xbox/mcpx/apu/vp/vp.c index 35d1219ac7..eed2c9b1e4 100644 --- a/hw/xbox/mcpx/apu/vp/vp.c +++ b/hw/xbox/mcpx/apu/vp/vp.c @@ -563,7 +563,8 @@ static void fe_method(MCPXAPUState *d, uint32_t method, uint32_t argument) DPRINTF("idle voice %d\n", argument); d->set_irq = true; } else { - assert(false); + DPRINTF("ignoring idle voice %d without FETFORCE1 request\n", + argument); } break; default: