mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
added menu on back select/start combo. audio choices in config
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <android/asset_manager_jni.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -179,6 +180,37 @@ static int GetTcgTbSizeFromEnv() {
|
||||
return static_cast<int>(parsed);
|
||||
}
|
||||
|
||||
static std::string ToLowerAscii(std::string value) {
|
||||
for (char& c : value) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<JNIEnv*>(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<jstring>(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<jstring>(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, "<init>", "(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.<init>") || !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, "<init>", "(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.<init>") || !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();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <toml++/toml.h>
|
||||
#include <android/log.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
@@ -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<char>(std::tolower(static_cast<unsigned char>(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>()) {
|
||||
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<std::string>()) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<String> = arrayOf(
|
||||
"SDL2",
|
||||
"xemu",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<string name="library_convert_copy_input_failed">Failed to copy ISO into converter workspace.</string>
|
||||
<string name="library_convert_copy_output_failed">Failed to copy converted XISO back to games folder.</string>
|
||||
<string name="library_convert_resolve_failed">Could not resolve destination folder for selected game.</string>
|
||||
<string name="library_restart_failed">Unable to restart. The selected game is no longer accessible.</string>
|
||||
<string name="library_game_size">Size: %1$s</string>
|
||||
<string name="library_game_path">Path: %1$s</string>
|
||||
<string name="library_open_game">Open</string>
|
||||
@@ -71,4 +72,12 @@
|
||||
<string name="library_about_url_fork" translatable="false">https://github.com/xemu-project/xemu</string>
|
||||
<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="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>
|
||||
<string name="in_game_menu_show_touch_controls">Show Touch Controls</string>
|
||||
<string name="in_game_menu_hide_touch_controls">Hide Touch Controls</string>
|
||||
<string name="in_game_menu_exit_to_library">Exit to Game Library</string>
|
||||
<string name="in_game_menu_quit_app">Quit App</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,4 +17,13 @@
|
||||
<item name="colorOutline">@color/xemu_outline</item>
|
||||
<item name="colorOutlineVariant">@color/xemu_outline_variant</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Xemu.RoundedDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Xemu.RoundedDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Xemu.RoundedDialog" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">24dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
+24
-16
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user