From 55262dde6d062670b974c3fe9870652fbf520b42 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Tue, 17 Feb 2026 19:29:18 -0500 Subject: [PATCH] added menu on back select/start combo. audio choices in config --- android/app/build.gradle.kts | 4 +- android/app/src/main/cpp/xemu_android.cpp | 318 ++++++++++++++---- .../app/src/main/cpp/xemu_settings_android.cc | 20 ++ .../izzy2lost/x1box/GameLibraryActivity.kt | 51 +++ .../java/com/izzy2lost/x1box/MainActivity.kt | 131 ++++++++ android/app/src/main/res/values/strings.xml | 9 + android/app/src/main/res/values/themes.xml | 9 + hw/xbox/mcpx/apu/apu.c | 40 ++- hw/xbox/mcpx/apu/vp/hrtf.h | 7 + hw/xbox/mcpx/apu/vp/vp.c | 1 + ui/xemu.c | 14 +- 11 files changed, 517 insertions(+), 87 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index bd7c0dbdb7..71127efef5 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -30,8 +30,8 @@ android { minSdk = 26 targetSdk = 36 - versionCode = 5 - versionName = "1.0.4" + versionCode = 6 + versionName = "1.0.5" ndk { abiFilters += listOf("arm64-v8a") diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index fce15337d7..a2b1f4f99f 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -179,6 +180,37 @@ static int GetTcgTbSizeFromEnv() { return static_cast(parsed); } +static std::string ToLowerAscii(std::string value) { + for (char& c : value) { + c = static_cast(std::tolower(static_cast(c))); + } + return value; +} + +static std::string ResolveAndroidAudioDriverHint() { + constexpr const char* kDefaultAudioDriverHint = "openslES,aaudio,android"; + const char* value = SDL_getenv("XEMU_ANDROID_AUDIO_DRIVER"); + if (!value || value[0] == '\0') { + return kDefaultAudioDriverHint; + } + + std::string raw(value); + std::string normalized = ToLowerAscii(raw); + if (normalized == "auto" || normalized == "default") { + return kDefaultAudioDriverHint; + } + if (normalized == "opensl" || normalized == "opensles") { + return "openslES,aaudio,android"; + } + if (normalized == "aaudio") { + return "aaudio,openslES,android"; + } + if (normalized == "android" || normalized == "audiotrack") { + return "android,openslES,aaudio"; + } + return raw; +} + static JNIEnv* GetEnv() { return static_cast(SDL_AndroidGetJNIEnv()); } @@ -229,83 +261,231 @@ static bool ShouldEnableInlineAioWorkaround(const std::string& crash_flag_path) } static std::string GetPrefString(JNIEnv* env, jobject activity, const char* key) { - jclass activityClass = env->GetObjectClass(activity); - jmethodID getPrefs = env->GetMethodID(activityClass, "getSharedPreferences", - "(Ljava/lang/String;I)Landroid/content/SharedPreferences;"); - if (!getPrefs) return {}; - jstring prefsName = env->NewStringUTF(kPrefsName); - jobject prefs = env->CallObjectMethod(activity, getPrefs, prefsName, 0); - env->DeleteLocalRef(prefsName); - if (HasException(env, "getSharedPreferences") || !prefs) return {}; + if (!env || !activity || !key || key[0] == '\0') { + return {}; + } - jclass prefsClass = env->GetObjectClass(prefs); - jmethodID getString = env->GetMethodID( + std::string out; + jclass activityClass = nullptr; + jobject prefs = nullptr; + jclass prefsClass = nullptr; + jstring value = nullptr; + jmethodID getPrefs = nullptr; + jmethodID getString = nullptr; + + activityClass = env->GetObjectClass(activity); + if (!activityClass) { + return {}; + } + + getPrefs = env->GetMethodID(activityClass, "getSharedPreferences", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences;"); + if (!getPrefs) { + goto cleanup; + } + + { + jstring prefsName = env->NewStringUTF(kPrefsName); + if (!prefsName) { + goto cleanup; + } + prefs = env->CallObjectMethod(activity, getPrefs, prefsName, 0); + env->DeleteLocalRef(prefsName); + } + if (HasException(env, "getSharedPreferences") || !prefs) { + goto cleanup; + } + + prefsClass = env->GetObjectClass(prefs); + if (!prefsClass) { + goto cleanup; + } + getString = env->GetMethodID( prefsClass, "getString", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); - if (!getString) return {}; + if (!getString) { + goto cleanup; + } - jstring jkey = env->NewStringUTF(key); - jstring jdefault = nullptr; - jstring value = static_cast(env->CallObjectMethod(prefs, getString, jkey, jdefault)); - env->DeleteLocalRef(jkey); - if (HasException(env, "SharedPreferences.getString")) return {}; + { + jstring jkey = env->NewStringUTF(key); + if (!jkey) { + goto cleanup; + } + jstring jdefault = nullptr; + value = static_cast(env->CallObjectMethod(prefs, getString, jkey, jdefault)); + env->DeleteLocalRef(jkey); + } + if (HasException(env, "SharedPreferences.getString")) { + goto cleanup; + } - std::string out = JStringToString(env, value); + out = JStringToString(env, value); + +cleanup: if (value) env->DeleteLocalRef(value); + if (prefsClass) env->DeleteLocalRef(prefsClass); + if (prefs) env->DeleteLocalRef(prefs); + if (activityClass) env->DeleteLocalRef(activityClass); return out; } static bool CopyUriToPath(JNIEnv* env, jobject activity, const std::string& uriString, const std::string& path) { - if (uriString.empty() || path.empty()) return false; + if (!env || !activity || uriString.empty() || path.empty()) return false; - jclass activityClass = env->GetObjectClass(activity); - jmethodID getContentResolver = env->GetMethodID(activityClass, "getContentResolver", - "()Landroid/content/ContentResolver;"); - if (!getContentResolver) return false; - jobject resolver = env->CallObjectMethod(activity, getContentResolver); - if (HasException(env, "getContentResolver") || !resolver) return false; + bool success = false; + jclass activityClass = nullptr; + jobject resolver = nullptr; + jclass uriClass = nullptr; + jobject uri = nullptr; + jclass resolverClass = nullptr; + jobject inputStream = nullptr; + jclass fosClass = nullptr; + jobject outputStream = nullptr; + jclass inputClass = nullptr; + jclass outputClass = nullptr; + jbyteArray buffer = nullptr; + jmethodID readMethod = nullptr; + jmethodID writeMethod = nullptr; + jmethodID closeInput = nullptr; + jmethodID closeOutput = nullptr; - jclass uriClass = env->FindClass("android/net/Uri"); - jmethodID parse = env->GetStaticMethodID(uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); - jstring juri = env->NewStringUTF(uriString.c_str()); - jobject uri = env->CallStaticObjectMethod(uriClass, parse, juri); - env->DeleteLocalRef(juri); - if (HasException(env, "Uri.parse") || !uri) return false; - - jclass resolverClass = env->GetObjectClass(resolver); - jmethodID openInputStream = env->GetMethodID( - resolverClass, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;"); - jobject inputStream = env->CallObjectMethod(resolver, openInputStream, uri); - if (HasException(env, "openInputStream") || !inputStream) return false; - - jclass fosClass = env->FindClass("java/io/FileOutputStream"); - jmethodID fosCtor = env->GetMethodID(fosClass, "", "(Ljava/lang/String;)V"); - jstring jpath = env->NewStringUTF(path.c_str()); - jobject outputStream = env->NewObject(fosClass, fosCtor, jpath); - env->DeleteLocalRef(jpath); - if (HasException(env, "FileOutputStream.") || !outputStream) return false; - - jclass inputClass = env->GetObjectClass(inputStream); - jclass outputClass = env->GetObjectClass(outputStream); - jmethodID readMethod = env->GetMethodID(inputClass, "read", "([B)I"); - jmethodID closeInput = env->GetMethodID(inputClass, "close", "()V"); - jmethodID writeMethod = env->GetMethodID(outputClass, "write", "([BII)V"); - jmethodID closeOutput = env->GetMethodID(outputClass, "close", "()V"); - if (!readMethod || !writeMethod) return false; - - const int kBufferSize = 64 * 1024; - jbyteArray buffer = env->NewByteArray(kBufferSize); - while (true) { - jint read = env->CallIntMethod(inputStream, readMethod, buffer); - if (HasException(env, "InputStream.read")) break; - if (read <= 0) break; - env->CallVoidMethod(outputStream, writeMethod, buffer, 0, read); - if (HasException(env, "OutputStream.write")) break; + activityClass = env->GetObjectClass(activity); + if (!activityClass) { + goto cleanup; } - env->DeleteLocalRef(buffer); - env->CallVoidMethod(inputStream, closeInput); - env->CallVoidMethod(outputStream, closeOutput); - HasException(env, "close streams"); - return true; + + { + jmethodID getContentResolver = env->GetMethodID(activityClass, "getContentResolver", + "()Landroid/content/ContentResolver;"); + if (!getContentResolver) { + goto cleanup; + } + resolver = env->CallObjectMethod(activity, getContentResolver); + if (HasException(env, "getContentResolver") || !resolver) { + goto cleanup; + } + } + + uriClass = env->FindClass("android/net/Uri"); + if (!uriClass) { + goto cleanup; + } + { + jmethodID parse = env->GetStaticMethodID(uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); + if (!parse) { + goto cleanup; + } + jstring juri = env->NewStringUTF(uriString.c_str()); + if (!juri) { + goto cleanup; + } + uri = env->CallStaticObjectMethod(uriClass, parse, juri); + env->DeleteLocalRef(juri); + if (HasException(env, "Uri.parse") || !uri) { + goto cleanup; + } + } + + resolverClass = env->GetObjectClass(resolver); + if (!resolverClass) { + goto cleanup; + } + { + jmethodID openInputStream = env->GetMethodID( + resolverClass, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;"); + if (!openInputStream) { + goto cleanup; + } + inputStream = env->CallObjectMethod(resolver, openInputStream, uri); + if (HasException(env, "openInputStream") || !inputStream) { + goto cleanup; + } + } + + fosClass = env->FindClass("java/io/FileOutputStream"); + if (!fosClass) { + goto cleanup; + } + { + jmethodID fosCtor = env->GetMethodID(fosClass, "", "(Ljava/lang/String;)V"); + if (!fosCtor) { + goto cleanup; + } + jstring jpath = env->NewStringUTF(path.c_str()); + if (!jpath) { + goto cleanup; + } + outputStream = env->NewObject(fosClass, fosCtor, jpath); + env->DeleteLocalRef(jpath); + if (HasException(env, "FileOutputStream.") || !outputStream) { + goto cleanup; + } + } + + inputClass = env->GetObjectClass(inputStream); + outputClass = env->GetObjectClass(outputStream); + if (!inputClass || !outputClass) { + goto cleanup; + } + readMethod = env->GetMethodID(inputClass, "read", "([B)I"); + closeInput = env->GetMethodID(inputClass, "close", "()V"); + writeMethod = env->GetMethodID(outputClass, "write", "([BII)V"); + closeOutput = env->GetMethodID(outputClass, "close", "()V"); + if (!readMethod || !writeMethod || !closeInput || !closeOutput) { + goto cleanup; + } + + { + const int kBufferSize = 64 * 1024; + buffer = env->NewByteArray(kBufferSize); + if (!buffer) { + goto cleanup; + } + + while (true) { + jint read = env->CallIntMethod(inputStream, readMethod, buffer); + if (HasException(env, "InputStream.read")) { + goto cleanup; + } + if (read < 0) { + break; + } + if (read == 0) { + continue; + } + env->CallVoidMethod(outputStream, writeMethod, buffer, 0, read); + if (HasException(env, "OutputStream.write")) { + goto cleanup; + } + } + success = true; + } + +cleanup: + if (buffer) env->DeleteLocalRef(buffer); + if (inputStream && closeInput) { + env->CallVoidMethod(inputStream, closeInput); + if (HasException(env, "InputStream.close")) { + success = false; + } + } + if (outputStream && closeOutput) { + env->CallVoidMethod(outputStream, closeOutput); + if (HasException(env, "OutputStream.close")) { + success = false; + } + } + if (outputClass) env->DeleteLocalRef(outputClass); + if (inputClass) env->DeleteLocalRef(inputClass); + if (outputStream) env->DeleteLocalRef(outputStream); + if (fosClass) env->DeleteLocalRef(fosClass); + if (inputStream) env->DeleteLocalRef(inputStream); + if (resolverClass) env->DeleteLocalRef(resolverClass); + if (uri) env->DeleteLocalRef(uri); + if (uriClass) env->DeleteLocalRef(uriClass); + if (resolver) env->DeleteLocalRef(resolver); + if (activityClass) env->DeleteLocalRef(activityClass); + return success; } struct SetupFiles { @@ -388,6 +568,9 @@ static bool WriteConfigToml(const std::string& config_path, if (!android->contains("tcg_tb_size")) { android->insert_or_assign("tcg_tb_size", 128); } + if (!android->contains("audio_driver")) { + android->insert_or_assign("audio_driver", "auto"); + } files->insert_or_assign("bootrom_path", mcpx); files->insert_or_assign("flashrom_path", flash); @@ -549,9 +732,10 @@ extern "C" int SDL_main(int argc, char* argv[]) { (void)argv; LogInfo("SDL_main: start"); - // Prefer AAudio on Android, but keep Android AudioTrack as fallback. - SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, "aaudio,android", + std::string audio_driver_hint = ResolveAndroidAudioDriverHint(); + SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, audio_driver_hint.c_str(), SDL_HINT_OVERRIDE); + LogInfoFmt("SDL_HINT_AUDIODRIVER=%s", audio_driver_hint.c_str()); SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); SDL_DisableScreenSaver(); diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index bbf3e4f661..d7358cfe89 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -142,6 +143,14 @@ static bool parse_filtering(const std::string &value, CONFIG_DISPLAY_FILTERING * return false; } +static std::string to_lower_ascii(std::string value) +{ + for (char &c : value) { + c = static_cast(std::tolower(static_cast(c))); + } + return value; +} + const char *xemu_settings_get_error_message(void) { return error_msg.empty() ? NULL : error_msg.c_str(); @@ -358,6 +367,17 @@ bool xemu_settings_load(void) snprintf(fifo_str, sizeof(fifo_str), "%d", fifo_frames); setenv("XEMU_ANDROID_AUDIO_FIFO_FRAMES", fifo_str, 1); } + if (auto audio_driver = android_cfg["audio_driver"].value()) { + std::string driver = *audio_driver; + std::string normalized = to_lower_ascii(driver); + if (normalized == "audiotrack") { + setenv("XEMU_ANDROID_AUDIO_DRIVER", "android", 1); + } else if (normalized == "opensl") { + setenv("XEMU_ANDROID_AUDIO_DRIVER", "opensles", 1); + } else if (!driver.empty()) { + setenv("XEMU_ANDROID_AUDIO_DRIVER", driver.c_str(), 1); + } + } // System file paths if (auto bootrom = sys_files["bootrom_path"].value()) { 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 a826df5873..9bbf250434 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt @@ -35,6 +35,10 @@ import java.util.Locale import java.util.concurrent.ConcurrentHashMap class GameLibraryActivity : AppCompatActivity() { + companion object { + const val EXTRA_RESTART_LAST_GAME = "com.izzy2lost.x1box.extra.RESTART_LAST_GAME" + } + private data class GameEntry( val title: String, val uri: Uri, @@ -95,6 +99,10 @@ class GameLibraryActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_game_library) + if (tryRestartLastGameFromIntent()) { + return + } + folderText = findViewById(R.id.library_folder_text) loadingSpinner = findViewById(R.id.library_loading) loadingText = findViewById(R.id.library_loading_text) @@ -160,6 +168,49 @@ class GameLibraryActivity : AppCompatActivity() { loadGames() } + private fun tryRestartLastGameFromIntent(): Boolean { + if (!intent.getBooleanExtra(EXTRA_RESTART_LAST_GAME, false)) { + return false + } + + val internalDvdIso = resolveInternalDvdIsoFile() + if (internalDvdIso == null || !internalDvdIso.isFile) { + Toast.makeText(this, getString(R.string.library_restart_failed), Toast.LENGTH_SHORT).show() + return false + } + + prefs.edit() + .putString("dvdPath", internalDvdIso.absolutePath) + .remove("dvdUri") + .putBoolean("skip_game_picker", false) + .apply() + + launchMainActivityForRestart() + return true + } + + private fun resolveInternalDvdIsoFile(): File? { + val external = getExternalFilesDir(null) + if (external != null) { + val externalIso = File(external, "x1box/dvd.iso") + if (externalIso.isFile) { + return externalIso + } + } + + val internalIso = File(filesDir, "x1box/dvd.iso") + if (internalIso.isFile) { + return internalIso + } + + return null + } + + private fun launchMainActivityForRestart() { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + private fun loadGames() { val folderUri = gamesFolderUri if (!isFolderReady(folderUri)) { 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 cdefcde9c9..8df4e6cc9d 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt @@ -1,14 +1,18 @@ package com.izzy2lost.x1box import android.content.Context +import android.content.Intent import android.hardware.input.InputManager import android.os.Build import android.os.Bundle import android.view.InputDevice +import android.view.KeyEvent import android.view.View import android.view.WindowInsets import android.view.WindowInsetsController import android.widget.FrameLayout +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.libsdl.app.SDLActivity class MainActivity : SDLActivity(), InputManager.InputDeviceListener { @@ -17,6 +21,10 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { private var isControllerVisible = false private var inputManager: InputManager? = null private var hasPhysicalController = false + private var inGameMenuDialog: AlertDialog? = null + private var startButtonDown = false + private var selectButtonDown = false + private var comboTriggered = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,6 +40,34 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { } } + override fun onBackPressed() { + val currentDialog = inGameMenuDialog + if (currentDialog?.isShowing == true) { + currentDialog.dismiss() + return + } + showInGameMenu() + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.keyCode == KeyEvent.KEYCODE_BACK && !isGamepadKeyEvent(event)) { + if (event.action == KeyEvent.ACTION_UP && event.repeatCount == 0) { + val currentDialog = inGameMenuDialog + if (currentDialog?.isShowing == true) { + currentDialog.dismiss() + } else { + showInGameMenu() + } + } + return true + } + + if (handleGamepadMenuCombo(event)) { + return true + } + return super.dispatchKeyEvent(event) + } + private fun hideSystemUI() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11 (API 30) and above @@ -164,6 +200,9 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { } override fun onDestroy() { + inGameMenuDialog?.dismiss() + inGameMenuDialog = null + // Unregister virtual controller try { org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2) @@ -195,6 +234,98 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { checkForPhysicalControllers() } + private fun handleGamepadMenuCombo(event: KeyEvent): Boolean { + if (!isGamepadKeyEvent(event)) { + return false + } + + val isStartKey = event.keyCode == KeyEvent.KEYCODE_BUTTON_START + val isSelectKey = event.keyCode == KeyEvent.KEYCODE_BUTTON_SELECT || + event.keyCode == KeyEvent.KEYCODE_BACK + if (!isStartKey && !isSelectKey) { + return false + } + + when (event.action) { + KeyEvent.ACTION_DOWN -> { + if (isStartKey) { + startButtonDown = true + } + if (isSelectKey) { + selectButtonDown = true + } + + if (!comboTriggered && event.repeatCount == 0 && + startButtonDown && selectButtonDown) { + comboTriggered = true + showInGameMenu() + return true + } + } + KeyEvent.ACTION_UP -> { + if (isStartKey) { + startButtonDown = false + } + if (isSelectKey) { + selectButtonDown = false + } + if (!startButtonDown || !selectButtonDown) { + comboTriggered = false + } + } + } + + return comboTriggered + } + + private fun isGamepadKeyEvent(event: KeyEvent): Boolean { + val source = event.source + return ((source and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || + ((source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) + } + + private fun showInGameMenu() { + val options = arrayOf( + getString(R.string.in_game_menu_resume), + if (isControllerVisible) { + getString(R.string.in_game_menu_hide_touch_controls) + } else { + getString(R.string.in_game_menu_show_touch_controls) + }, + getString(R.string.in_game_menu_exit_to_library), + getString(R.string.in_game_menu_quit_app), + ) + + val dialog = MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Xemu_RoundedDialog) + .setTitle(getString(R.string.in_game_menu_title)) + .setItems(options) { _, which -> + when (which) { + 0 -> { + // Resume + } + 1 -> toggleOnScreenController() + 2 -> exitToGameLibrary() + 3 -> finishAffinity() + } + } + .setOnDismissListener { + inGameMenuDialog = null + hideSystemUI() + } + .create() + + inGameMenuDialog = dialog + dialog.show() + } + + private fun exitToGameLibrary() { + val intent = Intent(this, GameLibraryActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + startActivity(intent) + finish() + } + override fun getLibraries(): Array = arrayOf( "SDL2", "xemu", diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 1ce12ffaf8..5a2e01cf05 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -54,6 +54,7 @@ Failed to copy ISO into converter workspace. Failed to copy converted XISO back to games folder. Could not resolve destination folder for selected game. + Unable to restart. The selected game is no longer accessible. Size: %1$s Path: %1$s Open @@ -71,4 +72,12 @@ https://github.com/xemu-project/xemu https://github.com/izzy2lost/xemu/blob/master/LICENSE https://github.com/izzy2lost/xemu/blob/master/COPYING + + Game Menu + Resume + Restart Game + Show Touch Controls + Hide Touch Controls + Exit to Game Library + Quit App diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index eebaf59cf8..30da907506 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -17,4 +17,13 @@ @color/xemu_outline @color/xemu_outline_variant + + + + diff --git a/hw/xbox/mcpx/apu/apu.c b/hw/xbox/mcpx/apu/apu.c index 6d62d3d69f..107f9fa05b 100644 --- a/hw/xbox/mcpx/apu/apu.c +++ b/hw/xbox/mcpx/apu/apu.c @@ -135,12 +135,13 @@ static void throttle(MCPXAPUState *d) } if (queued_bytes > d->monitor.queued_bytes_low) { + int64_t now_us = qemu_clock_get_us(QEMU_CLOCK_REALTIME); if (d->next_frame_time_us == 0 || - start_us - d->next_frame_time_us > EP_FRAME_US) { - d->next_frame_time_us = start_us; + now_us - d->next_frame_time_us > EP_FRAME_US) { + d->next_frame_time_us = now_us; } while (!qatomic_read(&d->exiting)) { - int64_t now_us = qemu_clock_get_us(QEMU_CLOCK_REALTIME); + now_us = qemu_clock_get_us(QEMU_CLOCK_REALTIME); int64_t remaining_ms = (d->next_frame_time_us - now_us) / 1000; if (remaining_ms > 0) { int sleep_ms = remaining_ms > INT_MAX ? INT_MAX : (int)remaining_ms; @@ -211,12 +212,14 @@ static void se_frame(MCPXAPUState *d) } } - qemu_spin_lock(&d->monitor.fifo_lock); - int num_bytes_free = (int)fifo8_num_free(&d->monitor.fifo); - assert(num_bytes_free >= sizeof(d->monitor.frame_buf)); - fifo8_push_all(&d->monitor.fifo, (uint8_t *)d->monitor.frame_buf, - sizeof(d->monitor.frame_buf)); - qemu_spin_unlock(&d->monitor.fifo_lock); + if (d->monitor.fifo_capacity_bytes > 0) { + qemu_spin_lock(&d->monitor.fifo_lock); + int num_bytes_free = (int)fifo8_num_free(&d->monitor.fifo); + assert(num_bytes_free >= sizeof(d->monitor.frame_buf)); + fifo8_push_all(&d->monitor.fifo, (uint8_t *)d->monitor.frame_buf, + sizeof(d->monitor.frame_buf)); + qemu_spin_unlock(&d->monitor.fifo_lock); + } memset(d->monitor.frame_buf, 0, sizeof(d->monitor.frame_buf)); } @@ -310,6 +313,10 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b) static void monitor_init(MCPXAPUState *d) { qemu_spin_init(&d->monitor.fifo_lock); + d->monitor.fifo_capacity_bytes = 0; + d->monitor.device_buffer_bytes = 0; + d->monitor.queued_bytes_low = 0; + d->monitor.queued_bytes_high = 0; int fifo_frames = 3; int audio_samples = 512; @@ -323,7 +330,6 @@ static void monitor_init(MCPXAPUState *d) #endif int fifo_capacity_bytes = fifo_frames * sizeof(d->monitor.frame_buf); fifo8_create(&d->monitor.fifo, fifo_capacity_bytes); - d->monitor.fifo_capacity_bytes = fifo_capacity_bytes; struct SDL_AudioSpec sdl_audio_spec = { .freq = 48000, @@ -335,8 +341,9 @@ static void monitor_init(MCPXAPUState *d) }; if (SDL_Init(SDL_INIT_AUDIO) < 0) { - fprintf(stderr, "Failed to initialize SDL audio subsystem: %s\n", SDL_GetError()); - exit(1); + fprintf(stderr, "WARNING: Failed to initialize SDL audio subsystem: %s\n", + SDL_GetError()); + return; } SDL_AudioDeviceID sdl_audio_dev; @@ -344,9 +351,9 @@ static void monitor_init(MCPXAPUState *d) sdl_audio_dev = SDL_OpenAudioDevice(NULL, 0, &sdl_audio_spec, &obtained_audio_spec, 0); if (sdl_audio_dev == 0) { - fprintf(stderr, "SDL_OpenAudioDevice failed: %s\n", SDL_GetError()); - assert(!"SDL_OpenAudioDevice failed"); - exit(1); + fprintf(stderr, "WARNING: SDL_OpenAudioDevice failed: %s\n", + SDL_GetError()); + return; } int bytes_per_sample = SDL_AUDIO_BITSIZE(obtained_audio_spec.format) / 8; @@ -366,7 +373,8 @@ static void monitor_init(MCPXAPUState *d) int frame_bytes = sizeof(d->monitor.frame_buf); int drain_bytes = MAX(device_buffer_bytes, frame_bytes); - int max_high = MAX(d->monitor.fifo_capacity_bytes - frame_bytes, frame_bytes); + int max_high = MAX(fifo_capacity_bytes - frame_bytes, frame_bytes); + d->monitor.fifo_capacity_bytes = fifo_capacity_bytes; d->monitor.device_buffer_bytes = device_buffer_bytes; d->monitor.queued_bytes_high = MIN(3 * drain_bytes, max_high); d->monitor.queued_bytes_low = MIN(drain_bytes, d->monitor.queued_bytes_high); diff --git a/hw/xbox/mcpx/apu/vp/hrtf.h b/hw/xbox/mcpx/apu/vp/hrtf.h index 0274a5c61d..fa35c4840a 100644 --- a/hw/xbox/mcpx/apu/vp/hrtf.h +++ b/hw/xbox/mcpx/apu/vp/hrtf.h @@ -48,6 +48,13 @@ static inline void hrtf_filter_init(HrtfFilter *f) memset(f, 0, sizeof(*f)); } +static inline void hrtf_filter_clear_history(HrtfFilter *f) +{ + f->buf_pos = 0; + memset(f->ch[0].buf, 0, sizeof(f->ch[0].buf)); + memset(f->ch[1].buf, 0, sizeof(f->ch[1].buf)); +} + static inline void hrtf_filter_set_target_params(HrtfFilter *f, float hrir_coeff[2][HRTF_NUM_TAPS], float itd) diff --git a/hw/xbox/mcpx/apu/vp/vp.c b/hw/xbox/mcpx/apu/vp/vp.c index 3e92835ad1..591f729175 100644 --- a/hw/xbox/mcpx/apu/vp/vp.c +++ b/hw/xbox/mcpx/apu/vp/vp.c @@ -55,6 +55,7 @@ static void voice_reset_filters(MCPXAPUState *d, uint16_t v) { assert(v < MCPX_HW_MAX_VOICES); memset(&d->vp.filters[v].svf, 0, sizeof(d->vp.filters[v].svf)); + hrtf_filter_clear_history(&d->vp.filters[v].hrtf); if (d->vp.filters[v].resampler) { src_reset(d->vp.filters[v].resampler); } diff --git a/ui/xemu.c b/ui/xemu.c index 4ad3aa5869..d94ef1ce77 100644 --- a/ui/xemu.c +++ b/ui/xemu.c @@ -949,9 +949,19 @@ static void sdl2_display_very_early_init(DisplayOptions *o) #endif #ifdef __ANDROID__ - SDL_setenv("SDL_AUDIODRIVER", "aaudio", 0); + const char *audio_env = SDL_getenv("SDL_AUDIODRIVER"); + const char *audio_hint = SDL_GetHint(SDL_HINT_AUDIODRIVER); + if ((!audio_env || !audio_env[0]) && + (!audio_hint || !audio_hint[0])) { + SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, + "openslES,aaudio,android", + SDL_HINT_DEFAULT); + audio_hint = SDL_GetHint(SDL_HINT_AUDIODRIVER); + } __android_log_print(ANDROID_LOG_INFO, "xemu-android", - "SDL_AUDIODRIVER=%s", SDL_getenv("SDL_AUDIODRIVER")); + "SDL audio env=%s hint=%s", + (audio_env && audio_env[0]) ? audio_env : "(unset)", + (audio_hint && audio_hint[0]) ? audio_hint : "(unset)"); #endif if (SDL_Init(SDL_INIT_VIDEO)) {