diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ea7ff550a7..1a1bf3d5b4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -53,12 +53,10 @@
diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp
index 28f88c9241..db88fe3749 100644
--- a/android/app/src/main/cpp/xemu_android.cpp
+++ b/android/app/src/main/cpp/xemu_android.cpp
@@ -13,9 +13,11 @@
#include
#include
#include
+#include
#include
#include
#include
+#include
#include
#include
#include
@@ -36,34 +38,68 @@ extern "C" AddfdInfo* monitor_fdset_add_fd(int fd, bool has_fdset_id,
namespace {
constexpr const char* kLogTag = "xemu-android";
constexpr const char* kPrefsName = "x1box_prefs";
+constexpr const char* kDebugLogPrefKey = "setting_debug_logs_enabled";
+constexpr const char* kDebugLogRelativeDir = "x1box/debug-logs";
+constexpr const char* kNativeDebugLogFileName = "xemu-debug.log";
+constexpr off_t kMaxDebugLogBytes = 4 * 1024 * 1024;
static std::atomic g_qemu_init_started{false};
+static std::atomic g_native_debug_logging_enabled{false};
+static std::string g_native_debug_log_path;
+static SDL_mutex* g_native_debug_log_mutex = nullptr;
static JNIEnv* GetEnv();
static jobject GetActivity(JNIEnv* env);
static bool HasException(JNIEnv* env, const char* context);
+static std::string GetFilesDirPath(JNIEnv* env, jobject activity);
+static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity);
+static bool NativeDebugLoggingEnabled();
+static void AppendNativeDebugLog(const char* level, const char* message);
static void LogInfo(const char* msg) {
+ if (!NativeDebugLoggingEnabled()) {
+ return;
+ }
__android_log_print(ANDROID_LOG_INFO, kLogTag, "%s", msg);
+ AppendNativeDebugLog("I", msg);
}
static void LogInfoFmt(const char* fmt, const char* detail) {
+ if (!NativeDebugLoggingEnabled()) {
+ return;
+ }
__android_log_print(ANDROID_LOG_INFO, kLogTag, fmt, detail);
+ char buffer[1024] = {};
+ std::snprintf(buffer, sizeof(buffer), fmt, detail);
+ AppendNativeDebugLog("I", buffer);
}
static void LogInfoInt(const char* fmt, int value) {
+ if (!NativeDebugLoggingEnabled()) {
+ return;
+ }
__android_log_print(ANDROID_LOG_INFO, kLogTag, fmt, value);
+ char buffer[1024] = {};
+ std::snprintf(buffer, sizeof(buffer), fmt, value);
+ AppendNativeDebugLog("I", buffer);
}
static void LogError(const char* msg) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, "%s", msg);
+ AppendNativeDebugLog("E", msg);
}
static void LogErrorInt(const char* fmt, int value) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, fmt, value);
+ char buffer[1024] = {};
+ std::snprintf(buffer, sizeof(buffer), fmt, value);
+ AppendNativeDebugLog("E", buffer);
}
static void LogErrorFmt(const char* fmt, const char* detail) {
__android_log_print(ANDROID_LOG_ERROR, kLogTag, fmt, detail);
+ char buffer[1024] = {};
+ std::snprintf(buffer, sizeof(buffer), fmt, detail);
+ AppendNativeDebugLog("E", buffer);
}
static int g_next_dvd_fdset_id = 9000;
@@ -80,6 +116,43 @@ static bool FileExists(const std::string& path) {
return stat(path.c_str(), &st) == 0;
}
+static std::string CurrentNativeLogTimestamp() {
+ std::time_t now = std::time(nullptr);
+ std::tm local_time {};
+ localtime_r(&now, &local_time);
+ char buffer[32] = {};
+ std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &local_time);
+ return std::string(buffer);
+}
+
+static bool NativeDebugLoggingEnabled() {
+ return g_native_debug_logging_enabled.load() && !g_native_debug_log_path.empty();
+}
+
+static void AppendNativeDebugLog(const char* level, const char* message) {
+ if (!NativeDebugLoggingEnabled() || !level || !message || !g_native_debug_log_mutex) {
+ return;
+ }
+
+ SDL_LockMutex(g_native_debug_log_mutex);
+
+ struct stat st {};
+ const bool should_truncate =
+ stat(g_native_debug_log_path.c_str(), &st) == 0 &&
+ st.st_size > kMaxDebugLogBytes;
+
+ std::ofstream out(
+ g_native_debug_log_path,
+ should_truncate ? (std::ios::out | std::ios::trunc)
+ : (std::ios::out | std::ios::app));
+ if (out.is_open()) {
+ out << CurrentNativeLogTimestamp() << ' ' << level << '/' << kLogTag
+ << ": " << message << '\n';
+ }
+
+ SDL_UnlockMutex(g_native_debug_log_mutex);
+}
+
static bool IsTcgTuningEnabled() {
const char* value = SDL_getenv("XEMU_ANDROID_TCG_TUNING");
return !(value && value[0] == '0');
@@ -261,6 +334,57 @@ static std::string JStringToString(JNIEnv* env, jstring value) {
return out;
}
+static std::string GetFilesDirPath(JNIEnv* env, jobject activity) {
+ if (!env || !activity) {
+ return {};
+ }
+
+ jclass activityClass = env->GetObjectClass(activity);
+ if (!activityClass) {
+ return {};
+ }
+
+ jmethodID getFilesDir =
+ env->GetMethodID(activityClass, "getFilesDir", "()Ljava/io/File;");
+ env->DeleteLocalRef(activityClass);
+ if (!getFilesDir) {
+ return {};
+ }
+
+ jobject fileObj = env->CallObjectMethod(activity, getFilesDir);
+ if (HasException(env, "Activity.getFilesDir") || !fileObj) {
+ return {};
+ }
+
+ jclass fileClass = env->GetObjectClass(fileObj);
+ if (!fileClass) {
+ env->DeleteLocalRef(fileObj);
+ return {};
+ }
+
+ jmethodID getAbsolutePath =
+ env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;");
+ if (!getAbsolutePath) {
+ env->DeleteLocalRef(fileClass);
+ env->DeleteLocalRef(fileObj);
+ return {};
+ }
+
+ jstring pathValue = static_cast(
+ env->CallObjectMethod(fileObj, getAbsolutePath));
+ std::string path;
+ if (!HasException(env, "File.getAbsolutePath")) {
+ path = JStringToString(env, pathValue);
+ }
+
+ if (pathValue) {
+ env->DeleteLocalRef(pathValue);
+ }
+ env->DeleteLocalRef(fileClass);
+ env->DeleteLocalRef(fileObj);
+ return path;
+}
+
static bool HasInlineAioCrashFlag(const std::string& flag_path) {
if (flag_path.empty()) {
return false;
@@ -427,6 +551,31 @@ static int GetPrefInt(JNIEnv* env, jobject activity, const char* key, int defVal
return out;
}
+static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity) {
+ g_native_debug_logging_enabled.store(
+ GetPrefBool(env, activity, kDebugLogPrefKey, false));
+ g_native_debug_log_path.clear();
+
+ if (!g_native_debug_logging_enabled.load()) {
+ return;
+ }
+
+ const std::string files_dir = GetFilesDirPath(env, activity);
+ if (files_dir.empty()) {
+ g_native_debug_logging_enabled.store(false);
+ return;
+ }
+
+ const std::string log_dir = files_dir + "/" + kDebugLogRelativeDir;
+ EnsureDirExists(files_dir + "/x1box");
+ EnsureDirExists(log_dir);
+ g_native_debug_log_path = log_dir + "/" + kNativeDebugLogFileName;
+
+ if (!g_native_debug_log_mutex) {
+ g_native_debug_log_mutex = SDL_CreateMutex();
+ }
+}
+
static bool IsSeekableFd(int fd) {
errno = 0;
return lseek(fd, 0, SEEK_CUR) != static_cast(-1);
@@ -723,7 +872,7 @@ struct EmulatorSettings {
bool hrtf = true;
bool cache_shaders = true;
bool hard_fpu = true;
- bool vsync = true;
+ bool vsync = false;
bool skip_boot_anim = false;
bool network_enabled = false;
};
@@ -990,7 +1139,7 @@ static SetupFiles SyncSetupFiles() {
emuSettings.filtering = "nearest";
}
}
- emuSettings.vsync = GetPrefBool(env, activity, "setting_vsync", true);
+ emuSettings.vsync = GetPrefBool(env, activity, "setting_vsync", false);
out.audio_driver = GetPrefString(env, activity, "setting_audio_driver");
{
std::string normalized = ToLowerAscii(out.audio_driver);
@@ -1070,6 +1219,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
(void)argc;
(void)argv;
+ ConfigureNativeDebugLogging(GetEnv(), GetActivity(GetEnv()));
LogInfo("SDL_main: start");
std::string audio_driver_hint = ResolveAndroidAudioDriverHint();
SDL_SetHintWithPriority(SDL_HINT_AUDIODRIVER, audio_driver_hint.c_str(),
diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc
index 7618b3849e..41beaf0947 100644
--- a/android/app/src/main/cpp/xemu_settings_android.cc
+++ b/android/app/src/main/cpp/xemu_settings_android.cc
@@ -81,7 +81,7 @@ static void xemu_settings_apply_defaults(void)
CONFIG_DISPLAY_WINDOW_STARTUP_SIZE_1280X960;
g_config.display.window.last_width = 640;
g_config.display.window.last_height = 480;
- g_config.display.window.vsync = true;
+ g_config.display.window.vsync = false;
g_config.display.ui.show_menubar = true;
g_config.display.ui.show_notifications = true;
g_config.display.ui.hide_cursor = true;
diff --git a/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt b/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt
index 27cc5699e5..b0c5686924 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/ControllerInputBridge.kt
@@ -33,7 +33,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
}
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "Error on button press: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "Error on button press: ${e.message}" }
}
}
@@ -49,7 +49,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
}
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "Error on button release: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "Error on button release: ${e.message}" }
}
}
@@ -66,7 +66,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
}
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "Error on stick move: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "Error on stick move: ${e.message}" }
}
}
@@ -78,7 +78,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "Error on stick press: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "Error on stick press: ${e.message}" }
}
}
@@ -90,7 +90,7 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
}
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "Error on stick release: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "Error on stick release: ${e.message}" }
}
}
@@ -113,12 +113,12 @@ class ControllerInputBridge : OnScreenController.ControllerListener {
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
}
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "SDL pad event failed for $button: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "SDL pad event failed for $button: ${e.message}" }
}
try {
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, axis, axisValue)
} catch (e: Exception) {
- android.util.Log.e("ControllerBridge", "SDL joy event failed for $button: ${e.message}")
+ DebugLog.e("ControllerBridge", e) { "SDL joy event failed for $button: ${e.message}" }
}
}
diff --git a/android/app/src/main/java/com/izzy2lost/x1box/DebugLog.kt b/android/app/src/main/java/com/izzy2lost/x1box/DebugLog.kt
new file mode 100644
index 0000000000..10da3be08c
--- /dev/null
+++ b/android/app/src/main/java/com/izzy2lost/x1box/DebugLog.kt
@@ -0,0 +1,319 @@
+package com.izzy2lost.x1box
+
+import android.content.Context
+import android.util.Log
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.Executors
+
+object DebugLog {
+ const val PREF_ENABLED = "setting_debug_logs_enabled"
+
+ private const val TAG = "xemu-android"
+ private const val LOG_DIR = "x1box/debug-logs"
+ private const val UI_LOG_FILE_NAME = "ui-debug.log"
+ private const val NATIVE_LOG_FILE_NAME = "xemu-debug.log"
+ private const val UI_LOGCAT_FILE_NAME = "ui-logcat.log"
+ private const val XEMU_LOGCAT_FILE_NAME = "xemu-logcat.log"
+ private const val MAX_LOG_BYTES = 16L * 1024L * 1024L
+
+ @Volatile private var appContext: Context? = null
+ @PublishedApi
+ @Volatile
+ internal var enabled = false
+ @Volatile private var logcatProcess: java.lang.Process? = null
+ @Volatile private var logcatThread: Thread? = null
+ @Volatile private var activeLogcatPath: String? = null
+
+ private val writerExecutor = Executors.newSingleThreadExecutor { runnable ->
+ Thread(runnable, "x1box-debug-log-writer").apply {
+ isDaemon = true
+ }
+ }
+
+ fun initialize(context: Context) {
+ val applicationContext = context.applicationContext
+ appContext = applicationContext
+ enabled = applicationContext
+ .getSharedPreferences("x1box_prefs", Context.MODE_PRIVATE)
+ .getBoolean(PREF_ENABLED, false)
+ ensureLogcatCaptureState(applicationContext)
+ }
+
+ fun setEnabled(context: Context, value: Boolean, resetLogs: Boolean = false) {
+ initialize(context)
+ if (!value) {
+ stopLogcatCapture()
+ }
+ if (resetLogs) {
+ clearLogs(context)
+ }
+ enabled = value
+ if (value) {
+ ensureLogcatCaptureState(context.applicationContext)
+ }
+ if (value) {
+ i(TAG) { "Debug logging enabled" }
+ } else {
+ Log.i(TAG, "Debug logging disabled")
+ }
+ }
+
+ fun hasAnyLog(context: Context): Boolean {
+ return uiLogFile(context).isFile ||
+ nativeLogFile(context).isFile ||
+ uiLogcatFile(context).isFile ||
+ xemuLogcatFile(context).isFile
+ }
+
+ fun exportDefaultFileName(): String {
+ val stamp = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(Date())
+ return "x1box-debug-$stamp.log"
+ }
+
+ @Throws(Exception::class)
+ fun exportCombined(context: Context, outputStream: OutputStream) {
+ val uiLog = uiLogFile(context)
+ val nativeLog = nativeLogFile(context)
+ if (!uiLog.isFile && !nativeLog.isFile) {
+ throw IllegalStateException("No debug log captured yet.")
+ }
+
+ outputStream.bufferedWriter().use { writer ->
+ if (uiLog.isFile) {
+ writer.appendLine("=== UI Debug Log ===")
+ uiLog.bufferedReader().useLines { lines ->
+ lines.forEach(writer::appendLine)
+ }
+ }
+
+ if (nativeLog.isFile) {
+ if (uiLog.isFile) {
+ writer.appendLine()
+ }
+ writer.appendLine("=== xemu Native Debug Log ===")
+ nativeLog.bufferedReader().useLines { lines ->
+ lines.forEach(writer::appendLine)
+ }
+ }
+
+ val uiLogcat = uiLogcatFile(context)
+ if (uiLogcat.isFile) {
+ writer.appendLine()
+ writer.appendLine("=== UI Logcat Capture ===")
+ uiLogcat.bufferedReader().useLines { lines ->
+ lines.forEach(writer::appendLine)
+ }
+ }
+
+ val xemuLogcat = xemuLogcatFile(context)
+ if (xemuLogcat.isFile) {
+ writer.appendLine()
+ writer.appendLine("=== xemu Logcat Capture ===")
+ xemuLogcat.bufferedReader().useLines { lines ->
+ lines.forEach(writer::appendLine)
+ }
+ }
+ }
+ }
+
+ fun clearLogs(context: Context? = appContext) {
+ context ?: return
+ stopLogcatCapture()
+ uiLogFile(context).delete()
+ nativeLogFile(context).delete()
+ uiLogcatFile(context).delete()
+ xemuLogcatFile(context).delete()
+ }
+
+ inline fun d(tag: String, message: () -> String) {
+ if (!enabled) {
+ return
+ }
+ val text = message()
+ Log.d(tag, text)
+ appendUiLine("D", tag, text)
+ }
+
+ inline fun i(tag: String, message: () -> String) {
+ if (!enabled) {
+ return
+ }
+ val text = message()
+ Log.i(tag, text)
+ appendUiLine("I", tag, text)
+ }
+
+ inline fun w(tag: String, message: () -> String) {
+ if (!enabled) {
+ return
+ }
+ val text = message()
+ Log.w(tag, text)
+ appendUiLine("W", tag, text)
+ }
+
+ inline fun e(tag: String, throwable: Throwable? = null, message: () -> String) {
+ val text = message()
+ if (throwable != null) {
+ Log.e(tag, text, throwable)
+ } else {
+ Log.e(tag, text)
+ }
+ if (enabled) {
+ appendUiLine("E", tag, text, throwable)
+ }
+ }
+
+ fun nativeLogFile(context: Context): File {
+ return File(logDir(context), NATIVE_LOG_FILE_NAME)
+ }
+
+ @PublishedApi
+ internal fun appendUiLine(
+ level: String,
+ tag: String,
+ message: String,
+ throwable: Throwable? = null,
+ ) {
+ val context = appContext ?: return
+ writerExecutor.execute {
+ try {
+ val file = uiLogFile(context)
+ file.parentFile?.mkdirs()
+ trimFileIfNeeded(file)
+ file.appendText(
+ buildString {
+ append(timestamp())
+ append(' ')
+ append(level)
+ append('/')
+ append(tag)
+ append(": ")
+ appendLine(message)
+ if (throwable != null) {
+ appendLine(stackTraceFor(throwable))
+ }
+ },
+ Charsets.UTF_8
+ )
+ } catch (_: Exception) {
+ }
+ }
+ }
+
+ private fun uiLogFile(context: Context): File {
+ return File(logDir(context), UI_LOG_FILE_NAME)
+ }
+
+ private fun uiLogcatFile(context: Context): File {
+ return File(logDir(context), UI_LOGCAT_FILE_NAME)
+ }
+
+ private fun xemuLogcatFile(context: Context): File {
+ return File(logDir(context), XEMU_LOGCAT_FILE_NAME)
+ }
+
+ private fun currentProcessLogcatFile(context: Context): File {
+ val processName = runCatching {
+ File("/proc/self/cmdline").readText().trim('\u0000', ' ', '\n')
+ }.getOrDefault("")
+ return if (processName.endsWith(":xemu")) {
+ xemuLogcatFile(context)
+ } else {
+ uiLogcatFile(context)
+ }
+ }
+
+ private fun logDir(context: Context): File {
+ return File(context.filesDir, LOG_DIR)
+ }
+
+ private fun trimFileIfNeeded(file: File) {
+ if (file.isFile && file.length() > MAX_LOG_BYTES) {
+ file.writeText("")
+ }
+ }
+
+ private fun timestamp(): String {
+ return SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US).format(Date())
+ }
+
+ private fun stackTraceFor(throwable: Throwable): String {
+ return StringWriter().also { writer ->
+ PrintWriter(writer).use { printer ->
+ throwable.printStackTrace(printer)
+ }
+ }.toString().trimEnd()
+ }
+
+ private fun ensureLogcatCaptureState(context: Context) {
+ if (!enabled) {
+ stopLogcatCapture()
+ return
+ }
+
+ val targetFile = currentProcessLogcatFile(context)
+ if (activeLogcatPath == targetFile.absolutePath && logcatProcess != null) {
+ return
+ }
+
+ stopLogcatCapture()
+ startLogcatCapture(targetFile)
+ }
+
+ private fun startLogcatCapture(targetFile: File) {
+ try {
+ targetFile.parentFile?.mkdirs()
+ trimFileIfNeeded(targetFile)
+ val process = ProcessBuilder(
+ "logcat",
+ "-T",
+ "1",
+ "-v",
+ "threadtime",
+ "--pid=${android.os.Process.myPid()}",
+ )
+ .redirectErrorStream(true)
+ .start()
+
+ val thread = Thread({
+ try {
+ process.inputStream.bufferedReader().use { reader ->
+ FileOutputStream(targetFile, true).bufferedWriter(Charsets.UTF_8).use { writer ->
+ while (true) {
+ val line = reader.readLine() ?: break
+ writer.appendLine(line)
+ writer.flush()
+ }
+ }
+ }
+ } catch (_: Exception) {
+ }
+ }, "x1box-logcat-capture").apply {
+ isDaemon = true
+ start()
+ }
+
+ logcatProcess = process
+ logcatThread = thread
+ activeLogcatPath = targetFile.absolutePath
+ } catch (error: Exception) {
+ Log.w(TAG, "Failed to start logcat capture", error)
+ }
+ }
+
+ private fun stopLogcatCapture() {
+ logcatProcess?.destroy()
+ logcatProcess = null
+ logcatThread?.interrupt()
+ logcatThread = null
+ activeLogcatPath = null
+ }
+}
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 7e956b6693..d40ce049aa 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt
@@ -43,6 +43,8 @@ import java.util.concurrent.ConcurrentHashMap
class GameLibraryActivity : AppCompatActivity() {
companion object {
const val EXTRA_RESTART_LAST_GAME = "com.izzy2lost.x1box.extra.RESTART_LAST_GAME"
+ const val EXTRA_INITIAL_ORIENTATION =
+ "com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
private const val SNAPSHOT_PREVIEW_HEADER_SIZE = 12
private const val TOTAL_SNAPSHOT_SLOTS = 10
private const val XDVDFS_SECTOR_SIZE = 2048L
@@ -101,7 +103,12 @@ class GameLibraryActivity : AppCompatActivity() {
private val coverIndex = ConcurrentHashMap()
private val coverCollapsedIndex = ConcurrentHashMap()
private val coverEntries = ArrayList()
+ private val coverIndexLock = Any()
+ private val discFormatCacheLock = Any()
+ private val discFormatCache = HashMap()
@Volatile private var coverIndexLoaded = false
+ @Volatile private var discFormatCacheLoaded = false
+ @Volatile private var discFormatCacheDirty = false
private lateinit var loadingSpinner: ProgressBar
private lateinit var loadingText: TextView
@@ -148,6 +155,7 @@ class GameLibraryActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ applyInitialOrientationFromIntent()
OrientationLocker(this).enable()
setContentView(R.layout.activity_game_library)
EdgeToEdgeHelper.enable(this)
@@ -188,7 +196,11 @@ class GameLibraryActivity : AppCompatActivity() {
launchDashboard()
}
btnSettings.setOnClickListener {
- startActivity(Intent(this, SettingsActivity::class.java))
+ startActivity(
+ Intent(this, SettingsActivity::class.java).apply {
+ putExtra(SettingsActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
+ }
+ )
}
btnSnapshots.setOnClickListener {
showSnapshotStartupPicker()
@@ -219,6 +231,15 @@ class GameLibraryActivity : AppCompatActivity() {
loadGames()
}
+ private fun applyInitialOrientationFromIntent() {
+ val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
+ if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
+ initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ ) {
+ requestedOrientation = initialOrientation
+ }
+ }
+
private fun tryRestartLastGameFromIntent(): Boolean {
if (!intent.getBooleanExtra(EXTRA_RESTART_LAST_GAME, false)) {
return false
@@ -539,6 +560,7 @@ class GameLibraryActivity : AppCompatActivity() {
val generation = ++scanGeneration
Thread {
+ loadDiscFormatCacheIfNeeded()
val games = scanFolderForGames(readyFolderUri)
runOnUiThread {
if (generation != scanGeneration) {
@@ -813,47 +835,52 @@ class GameLibraryActivity : AppCompatActivity() {
if (coverIndexLoaded) {
return
}
- try {
- val lines = assets.open("X1_Covers.txt").bufferedReader().use { it.readLines() }
- val seenEntries = HashSet()
- for (line in lines) {
- val fileName = line.trim()
- if (fileName.isEmpty() || !fileName.endsWith(".png", ignoreCase = true)) {
- continue
- }
- val gameName = fileName.removeSuffix(".png").trim()
- val encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
- val url = coverRepoBaseUrl + encoded
-
- val exactKey = normalizeCoverKey(gameName)
- val strippedKey = stripTrailingRegion(exactKey)
- if (exactKey.isNotEmpty()) {
- coverIndex.putIfAbsent(exactKey, url)
- }
- if (strippedKey.isNotEmpty()) {
- coverIndex.putIfAbsent(strippedKey, url)
- }
- val canonical = if (strippedKey.isNotEmpty()) strippedKey else exactKey
- val collapsed = collapseCoverKey(canonical)
- if (collapsed.isNotEmpty()) {
- coverCollapsedIndex.putIfAbsent(collapsed, url)
- }
- if (canonical.isNotEmpty() && seenEntries.add("$canonical|$url")) {
- val tokens = tokenizeCoverKey(canonical)
- coverEntries.add(
- CoverEntry(
- collapsed = collapsed,
- tokens = tokens,
- numericTokens = tokens.filterTo(HashSet()) { token -> token.any(Char::isDigit) },
- url = url
- )
- )
- }
+ synchronized(coverIndexLock) {
+ if (coverIndexLoaded) {
+ return
}
- } catch (_: Exception) {
- // Keep empty index; grid will show placeholders if the asset is unavailable.
+ try {
+ val lines = assets.open("X1_Covers.txt").bufferedReader().use { it.readLines() }
+ val seenEntries = HashSet()
+ for (line in lines) {
+ val fileName = line.trim()
+ if (fileName.isEmpty() || !fileName.endsWith(".png", ignoreCase = true)) {
+ continue
+ }
+ val gameName = fileName.removeSuffix(".png").trim()
+ val encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20")
+ val url = coverRepoBaseUrl + encoded
+
+ val exactKey = normalizeCoverKey(gameName)
+ val strippedKey = stripTrailingRegion(exactKey)
+ if (exactKey.isNotEmpty()) {
+ coverIndex.putIfAbsent(exactKey, url)
+ }
+ if (strippedKey.isNotEmpty()) {
+ coverIndex.putIfAbsent(strippedKey, url)
+ }
+ val canonical = if (strippedKey.isNotEmpty()) strippedKey else exactKey
+ val collapsed = collapseCoverKey(canonical)
+ if (collapsed.isNotEmpty()) {
+ coverCollapsedIndex.putIfAbsent(collapsed, url)
+ }
+ if (canonical.isNotEmpty() && seenEntries.add("$canonical|$url")) {
+ val tokens = tokenizeCoverKey(canonical)
+ coverEntries.add(
+ CoverEntry(
+ collapsed = collapsed,
+ tokens = tokens,
+ numericTokens = tokens.filterTo(HashSet()) { token -> token.any(Char::isDigit) },
+ url = url
+ )
+ )
+ }
+ }
+ } catch (_: Exception) {
+ // Keep empty index; grid will show placeholders if the asset is unavailable.
+ }
+ coverIndexLoaded = true
}
- coverIndexLoaded = true
}
private fun normalizeLookupTitle(input: String): String {
@@ -1249,6 +1276,7 @@ class GameLibraryActivity : AppCompatActivity() {
stack.add(root to "")
val games = ArrayList()
+ val seenDiscFormatKeys = HashSet()
while (stack.isNotEmpty()) {
val (node, prefix) = stack.removeLast()
val files = try {
@@ -1265,22 +1293,128 @@ class GameLibraryActivity : AppCompatActivity() {
if (!child.isFile || !isSupportedGame(name)) {
continue
}
+ val sizeBytes = child.length()
games.add(
GameEntry(
title = toGameTitle(name),
uri = child.uri,
relativePath = prefix + name,
- sizeBytes = child.length(),
- discImageFormat = detectDiscImageFormat(child.uri, name),
+ sizeBytes = sizeBytes,
+ discImageFormat = resolveDiscImageFormat(child.uri, name, sizeBytes, seenDiscFormatKeys),
)
)
}
}
games.sortBy { it.title.lowercase(Locale.ROOT) }
+ persistDiscFormatCache(seenDiscFormatKeys)
return games
}
+ private fun discFormatCacheFile(): File = File(filesDir, "disc_format_cache.tsv")
+
+ private fun loadDiscFormatCacheIfNeeded() {
+ if (discFormatCacheLoaded) {
+ return
+ }
+ synchronized(discFormatCacheLock) {
+ if (discFormatCacheLoaded) {
+ return
+ }
+ val cacheFile = discFormatCacheFile()
+ if (cacheFile.isFile) {
+ runCatching {
+ cacheFile.forEachLine(Charsets.UTF_8) { line ->
+ val split = line.lastIndexOf('\t')
+ if (split <= 0 || split >= line.lastIndex) {
+ return@forEachLine
+ }
+ val key = line.substring(0, split)
+ val formatName = line.substring(split + 1)
+ runCatching { DiscImageFormat.valueOf(formatName) }
+ .getOrNull()
+ ?.let { format -> discFormatCache[key] = format }
+ }
+ }
+ }
+ discFormatCacheLoaded = true
+ }
+ }
+
+ private fun persistDiscFormatCache(seenKeys: Set) {
+ synchronized(discFormatCacheLock) {
+ if (!discFormatCacheLoaded) {
+ return
+ }
+
+ var changed = discFormatCacheDirty
+ val iterator = discFormatCache.keys.iterator()
+ while (iterator.hasNext()) {
+ if (iterator.next() !in seenKeys) {
+ iterator.remove()
+ changed = true
+ }
+ }
+
+ if (!changed) {
+ return
+ }
+
+ val cacheFile = discFormatCacheFile()
+ if (discFormatCache.isEmpty()) {
+ cacheFile.delete()
+ discFormatCacheDirty = false
+ return
+ }
+
+ runCatching {
+ cacheFile.parentFile?.mkdirs()
+ cacheFile.bufferedWriter(Charsets.UTF_8).use { writer ->
+ discFormatCache.entries.forEach { (key, format) ->
+ writer.append(key)
+ writer.append('\t')
+ writer.append(format.name)
+ writer.append('\n')
+ }
+ }
+ discFormatCacheDirty = false
+ }
+ }
+ }
+
+ private fun discFormatCacheKey(uri: Uri, sizeBytes: Long): String =
+ uri.toString() + "\t" + sizeBytes
+
+ private fun resolveDiscImageFormat(
+ uri: Uri,
+ fileName: String,
+ sizeBytes: Long,
+ seenKeys: MutableSet,
+ ): DiscImageFormat {
+ val lower = fileName.lowercase(Locale.ROOT)
+ if (!isDiscImageFormatDetectable(lower)) {
+ return DiscImageFormat.OTHER
+ }
+
+ val cacheKey = discFormatCacheKey(uri, sizeBytes)
+ seenKeys.add(cacheKey)
+
+ synchronized(discFormatCacheLock) {
+ discFormatCache[cacheKey]?.let { cached ->
+ return cached
+ }
+ }
+
+ val detected = detectDiscImageFormat(uri, fileName)
+ synchronized(discFormatCacheLock) {
+ if (discFormatCache[cacheKey] != detected) {
+ discFormatCache[cacheKey] = detected
+ discFormatCacheDirty = true
+ }
+ }
+ return detected
+ }
+
private fun detectDiscImageFormat(uri: Uri, fileName: String): DiscImageFormat {
val lower = fileName.lowercase(Locale.ROOT)
if (!isDiscImageFormatDetectable(lower)) {
diff --git a/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt
index 037fd496ed..7ac92141d9 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
-import android.util.Log
import android.widget.Toast
import java.io.File
@@ -15,6 +14,7 @@ class LauncherActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ DebugLog.initialize(this)
OrientationLocker(this).enable()
val prefs = getSharedPreferences("x1box_prefs", MODE_PRIVATE)
@@ -113,16 +113,16 @@ class LauncherActivity : Activity() {
.commit()
if (hasMcpx && hasFlash && hasHdd) {
- Log.i(TAG, "Frontend launch resolved via ${frontendLaunch.source}")
+ DebugLog.i(TAG) { "Frontend launch resolved via ${frontendLaunch.source}" }
startActivity(Intent(this, MainActivity::class.java))
finish()
return
}
- Log.i(TAG, "Frontend launch queued, but core setup is incomplete")
+ DebugLog.i(TAG) { "Frontend launch queued, but core setup is incomplete" }
Toast.makeText(this, R.string.frontend_launch_setup_required, Toast.LENGTH_SHORT).show()
} else if (hasExternalLaunchPayload(intent)) {
- Log.w(TAG, "Frontend intent received but no accessible game target was resolved")
+ DebugLog.w(TAG) { "Frontend intent received but no accessible game target was resolved" }
Toast.makeText(this, R.string.frontend_launch_unresolved, Toast.LENGTH_LONG).show()
}
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 6f0c25a476..1878a0f7ce 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt
@@ -8,7 +8,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Process
-import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
@@ -78,6 +77,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ DebugLog.initialize(this)
OrientationLocker(this, landscapeOnly = true).enable()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val requestedSlot = intent?.getIntExtra(EXTRA_AUTO_LOAD_SNAPSHOT_SLOT, 0) ?: 0
@@ -265,9 +265,9 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
0, // nhats
0 // nballs
)
- android.util.Log.d("MainActivity", "Virtual controller registered successfully")
+ DebugLog.d("MainActivity") { "Virtual controller registered successfully" }
} catch (e: Exception) {
- android.util.Log.e("MainActivity", "Failed to register virtual controller: ${e.message}")
+ DebugLog.e("MainActivity", e) { "Failed to register virtual controller: ${e.message}" }
}
}
@@ -329,7 +329,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
override fun onDestroy() {
- Log.i(TAG, "onDestroy()")
+ DebugLog.i(TAG) { "onDestroy()" }
swipeUpGestureRecognizer.reset()
inGameMenuDialog?.dismiss()
inGameMenuDialog = null
@@ -343,7 +343,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
try {
org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2)
} catch (e: Exception) {
- android.util.Log.e("MainActivity", "Failed to unregister virtual controller: ${e.message}")
+ DebugLog.e("MainActivity", e) { "Failed to unregister virtual controller: ${e.message}" }
}
inputManager?.unregisterInputDeviceListener(this)
@@ -352,17 +352,17 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
override fun onUserLeaveHint() {
- Log.i(TAG, "onUserLeaveHint()")
+ DebugLog.i(TAG) { "onUserLeaveHint()" }
super.onUserLeaveHint()
}
override fun onTrimMemory(level: Int) {
- Log.i(TAG, "onTrimMemory(level=$level)")
+ DebugLog.i(TAG) { "onTrimMemory(level=$level)" }
super.onTrimMemory(level)
}
override fun onLowMemory() {
- Log.w(TAG, "onLowMemory()")
+ DebugLog.w(TAG) { "onLowMemory()" }
super.onLowMemory()
}
@@ -803,6 +803,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private fun exitToGameLibrary() {
val intent = Intent(this, GameLibraryActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ putExtra(GameLibraryActivity.EXTRA_INITIAL_ORIENTATION, requestedOrientation)
}
startActivity(intent)
terminateXemuProcessSoon("exit to library")
@@ -826,7 +827,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
- Log.i(TAG, "Terminating :xemu process after $reason")
+ DebugLog.i(TAG) { "Terminating :xemu process after $reason" }
Process.killProcess(Process.myPid())
}.start()
}
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 c3dfb623b7..26ac67b0a1 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt
@@ -31,6 +31,8 @@ import java.util.zip.ZipInputStream
class SettingsActivity : AppCompatActivity() {
companion object {
+ const val EXTRA_INITIAL_ORIENTATION =
+ "com.izzy2lost.x1box.extra.INITIAL_ORIENTATION"
private const val PREF_ADVANCED_EXPERIMENTAL_EXPANDED = "settings_advanced_experimental_expanded"
private const val PREF_INSIGNIA_SETUP_URI = "setting_insignia_setup_assistant_uri"
private const val PREF_INSIGNIA_SETUP_NAME = "setting_insignia_setup_assistant_name"
@@ -129,6 +131,7 @@ class SettingsActivity : AppCompatActivity() {
private var isImportingDashboard = false
private var isPreparingInsignia = false
+ private lateinit var switchDebugLogs: MaterialSwitch
private lateinit var switchNetworkEnable: MaterialSwitch
private lateinit var tvVulkanDriverName: TextView
private lateinit var tvInsigniaStatus: TextView
@@ -205,9 +208,17 @@ class SettingsActivity : AppCompatActivity() {
launchInsigniaSetupAssistant(uri)
}
+ private val exportDebugLogDocument =
+ registerForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri: Uri? ->
+ uri ?: return@registerForActivityResult
+ exportDebugLog(uri)
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ applyInitialOrientationFromIntent()
OrientationLocker(this).enable()
+ DebugLog.initialize(this)
setContentView(R.layout.activity_settings)
EdgeToEdgeHelper.enable(this)
EdgeToEdgeHelper.applySystemBarPadding(findViewById(R.id.settings_scroll))
@@ -230,10 +241,12 @@ class SettingsActivity : AppCompatActivity() {
val switchFpu = findViewById(R.id.switch_hard_fpu)
val switchVsync = findViewById(R.id.switch_vsync)
val switchSkipBootAnim = findViewById(R.id.switch_skip_boot_anim)
+ switchDebugLogs = findViewById(R.id.switch_debug_logs)
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)
+ val btnExportDebugLog = findViewById(R.id.btn_export_debug_log)
val btnInitializeRetailHdd = findViewById(R.id.btn_initialize_retail_hdd)
switchNetworkEnable = findViewById(R.id.switch_network_enable)
tvInsigniaStatus = findViewById(R.id.tv_insignia_status)
@@ -317,9 +330,11 @@ class SettingsActivity : AppCompatActivity() {
switchHrtf.isChecked = prefs.getBoolean("setting_hrtf", true)
switchShaders.isChecked = prefs.getBoolean("setting_cache_shaders", true)
switchFpu.isChecked = prefs.getBoolean("setting_hard_fpu", true)
- switchVsync.isChecked = prefs.getBoolean("setting_vsync", true)
+ switchVsync.isChecked = prefs.getBoolean("setting_vsync", false)
switchSkipBootAnim.isChecked =
prefs.getBoolean("setting_skip_boot_anim", false)
+ switchDebugLogs.isChecked =
+ prefs.getBoolean(DebugLog.PREF_ENABLED, false)
switchNetworkEnable.isChecked =
prefs.getBoolean("setting_network_enable", false)
@@ -409,6 +424,8 @@ class SettingsActivity : AppCompatActivity() {
R.id.btn_filtering_nearest -> "nearest"
else -> "linear"
}
+ val wasDebugLoggingEnabled = prefs.getBoolean(DebugLog.PREF_ENABLED, false)
+ val enableDebugLogs = switchDebugLogs.isChecked
val edit = prefs.edit()
.putInt("setting_display_mode", selectedDisplayMode)
@@ -422,6 +439,7 @@ class SettingsActivity : AppCompatActivity() {
.putBoolean("setting_hard_fpu", switchFpu.isChecked)
.putBoolean("setting_vsync", switchVsync.isChecked)
.putBoolean("setting_skip_boot_anim", switchSkipBootAnim.isChecked)
+ .putBoolean(DebugLog.PREF_ENABLED, enableDebugLogs)
.putBoolean("setting_network_enable", switchNetworkEnable.isChecked)
.putString("setting_audio_driver", selectedAudioDriver)
.putString("setting_filtering", selectedFiltering)
@@ -437,6 +455,11 @@ class SettingsActivity : AppCompatActivity() {
}
edit.apply()
+ DebugLog.setEnabled(
+ context = this@SettingsActivity,
+ value = enableDebugLogs,
+ resetLogs = enableDebugLogs && !wasDebugLoggingEnabled
+ )
return applyEepromEdits()
}
@@ -444,6 +467,13 @@ class SettingsActivity : AppCompatActivity() {
btnClearCache.setOnClickListener {
showClearCacheConfirmation()
}
+ btnExportDebugLog.setOnClickListener {
+ if (!DebugLog.hasAnyLog(this)) {
+ Toast.makeText(this, R.string.settings_export_debug_log_empty, Toast.LENGTH_LONG).show()
+ return@setOnClickListener
+ }
+ exportDebugLogDocument.launch(DebugLog.exportDefaultFileName())
+ }
btnInitializeRetailHdd.setOnClickListener {
showInitializeHddLayoutPicker(btnInitializeRetailHdd)
@@ -456,6 +486,15 @@ class SettingsActivity : AppCompatActivity() {
}
}
+ private fun applyInitialOrientationFromIntent() {
+ val initialOrientation = intent.getIntExtra(EXTRA_INITIAL_ORIENTATION, Int.MIN_VALUE)
+ if (initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ||
+ initialOrientation == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ ) {
+ requestedOrientation = initialOrientation
+ }
+ }
+
private fun setAdvancedExperimentalExpanded(expanded: Boolean) {
layoutAdvancedExperimentalContent.visibility = if (expanded) View.VISIBLE else View.GONE
btnToggleAdvancedExperimental.text = getString(
@@ -468,6 +507,21 @@ class SettingsActivity : AppCompatActivity() {
prefs.edit().putBoolean(PREF_ADVANCED_EXPERIMENTAL_EXPANDED, expanded).apply()
}
+ private fun exportDebugLog(uri: Uri) {
+ try {
+ contentResolver.openOutputStream(uri, "w")?.use { stream ->
+ DebugLog.exportCombined(this, stream)
+ } ?: throw IOException("Could not open the selected export location.")
+ Toast.makeText(this, R.string.settings_export_debug_log_success, Toast.LENGTH_LONG).show()
+ } catch (error: Exception) {
+ Toast.makeText(
+ this,
+ getString(R.string.settings_export_debug_log_failed, error.message ?: "unknown error"),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
private fun openExternalLink(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
diff --git a/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt
index 77d892ab54..c2ec900fd3 100644
--- a/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt
+++ b/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt
@@ -4,7 +4,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.format.Formatter
-import android.util.Log
import android.view.View
import android.widget.TextView
import android.widget.Toast
@@ -152,6 +151,7 @@ class SetupWizardActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ DebugLog.initialize(this)
OrientationLocker(this).enable()
mcpxPath = loadValidatedLocalPath("mcpxPath", "mcpxUri", ::isSavedMcpxFileValid)
@@ -391,7 +391,7 @@ class SetupWizardActivity : AppCompatActivity() {
val base = getExternalFilesDir(null) ?: filesDir
val dir = File(base, "x1box")
if (!dir.exists() && !dir.mkdirs()) {
- Log.e("xemu-android", "Failed to create ${dir.absolutePath}")
+ DebugLog.e("xemu-android") { "Failed to create ${dir.absolutePath}" }
return null
}
val target = File(dir, destName)
@@ -403,7 +403,7 @@ class SetupWizardActivity : AppCompatActivity() {
} ?: return null
target.absolutePath
} catch (e: IOException) {
- Log.e("xemu-android", "Copy failed for $destName", e)
+ DebugLog.e("xemu-android", e) { "Copy failed for $destName" }
null
}
}
diff --git a/android/app/src/main/res/layout/activity_settings.xml b/android/app/src/main/res/layout/activity_settings.xml
index d8f6b9e7bb..0c30c31dbd 100644
--- a/android/app/src/main/res/layout/activity_settings.xml
+++ b/android/app/src/main/res/layout/activity_settings.xml
@@ -798,6 +798,30 @@
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green_light" />
+
+
+
+
+
+
Boot compatibility: this import did not include C:\\xboxdash.xbe, so %1$s will also be copied as xboxdash.xbe.
Warning: no obvious dashboard XBE was found anywhere under C:. Boot Dashboard may still show the service screen.
Maintenance
+ Enable Debug Logs
+ Writes extra UI and emulator bootstrap logs only while enabled. Turn this on before reproducing a problem, then export the log.
+ Export Debug Log
+ No debug log has been captured yet.
+ Debug log exported
+ Failed to export debug log: %1$s
Clear Cache
Clear emulator cache files now? This removes shader and app cache data, but keeps your games, saves, and HDD image.
Clear Cache
diff --git a/ui/xemu.c b/ui/xemu.c
index c9681519a1..41c683bb06 100644
--- a/ui/xemu.c
+++ b/ui/xemu.c
@@ -183,6 +183,10 @@ static bool sdl2_gl_has_extension(const char *ext_list, const char *ext)
static void android_log_gl_error(const char *stage)
{
+ if (!android_render_logs_enabled()) {
+ return;
+ }
+
GLenum err;
bool logged = false;
while ((err = glGetError()) != GL_NO_ERROR) {
@@ -1377,7 +1381,7 @@ void xemu_android_display_loop(void)
}
#ifdef __ANDROID__
xemu_android_refresh_frame_limit_from_env();
- SDL_GL_SetSwapInterval(1);
+ SDL_GL_SetSwapInterval(g_config.display.window.vsync ? 1 : 0);
xemu_hud_init(m_window, m_context);
#endif
tcg_register_init_ctx();