diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index b8611e5cb2..dade37cb33 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -11,10 +11,12 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -205,6 +207,69 @@ static std::string JStringToString(JNIEnv* env, jstring value) { return out; } +static std::string ToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +static std::string GetBuildField(JNIEnv* env, const char* field_name) { + jclass buildClass = env->FindClass("android/os/Build"); + if (!buildClass) { + HasException(env, "Build class lookup"); + return {}; + } + jfieldID field = env->GetStaticFieldID(buildClass, field_name, "Ljava/lang/String;"); + if (!field) { + HasException(env, field_name); + env->DeleteLocalRef(buildClass); + return {}; + } + jstring value = + static_cast(env->GetStaticObjectField(buildClass, field)); + if (HasException(env, field_name)) { + env->DeleteLocalRef(buildClass); + return {}; + } + std::string out = JStringToString(env, value); + if (value) { + env->DeleteLocalRef(value); + } + env->DeleteLocalRef(buildClass); + return out; +} + +static bool ShouldEnableInlineAioWorkaround() { + const char* forced = SDL_getenv("XEMU_ANDROID_INLINE_AIO"); + if (forced) { + return forced[0] != '\0' && forced[0] != '0'; + } + + JNIEnv* env = GetEnv(); + if (!env) { + return false; + } + + const std::string device = ToLowerAscii(GetBuildField(env, "DEVICE")); + const std::string product = ToLowerAscii(GetBuildField(env, "PRODUCT")); + const std::string model = ToLowerAscii(GetBuildField(env, "MODEL")); + + static const char* kAffectedDevices[] = { + "duchamp", + }; + + for (const char* marker : kAffectedDevices) { + if (device == marker || + product.find(marker) != std::string::npos || + model.find(marker) != std::string::npos) { + return true; + } + } + + return false; +} + static std::string GetPrefString(JNIEnv* env, jobject activity, const char* key) { jclass activityClass = env->GetObjectClass(activity); jmethodID getPrefs = env->GetMethodID(activityClass, "getSharedPreferences", @@ -324,10 +389,13 @@ static bool WriteConfigToml(const std::string& config_path, toml::table* general = EnsureTable(tbl, "general"); toml::table* display = EnsureTable(tbl, "display"); toml::table* display_window = EnsureTable(*display, "window"); + toml::table* audio = EnsureTable(tbl, "audio"); + toml::table* audio_vp = EnsureTable(*audio, "vp"); toml::table* android = EnsureTable(tbl, "android"); toml::table* sys = EnsureTable(tbl, "sys"); toml::table* files = EnsureTable(*sys, "files"); - if (!general || !display || !display_window || !android || !sys || !files) { + if (!general || !display || !display_window || !audio || !audio_vp || + !android || !sys || !files) { LogErrorFmt("Failed to build config tables at %s", config_path.c_str()); return false; } @@ -340,6 +408,15 @@ static bool WriteConfigToml(const std::string& config_path, if (!display_window->contains("vsync")) { display_window->insert_or_assign("vsync", false); } + if (!audio_vp->contains("num_workers")) { + audio_vp->insert_or_assign("num_workers", 0); + } + if (!audio->contains("hrtf")) { + audio->insert_or_assign("hrtf", true); + } + if (!audio->contains("volume_limit")) { + audio->insert_or_assign("volume_limit", 1.0); + } if (!android->contains("force_cpu_blit")) { android->insert_or_assign("force_cpu_blit", false); } @@ -524,6 +601,12 @@ extern "C" int SDL_main(int argc, char* argv[]) { SDL_GameControllerEventState(SDL_ENABLE); LoadGameControllerMappingsFromAssets(); + if (!SDL_getenv("XEMU_ANDROID_INLINE_AIO")) { + const bool use_inline_aio = ShouldEnableInlineAioWorkaround(); + setenv("XEMU_ANDROID_INLINE_AIO", use_inline_aio ? "1" : "0", 1); + LogInfoFmt("XEMU_ANDROID_INLINE_AIO=%s", use_inline_aio ? "1" : "0"); + } + SetupFiles setup = SyncSetupFiles(); if (!setup.config_path.empty()) { diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index be1b6ef08d..bbf3e4f661 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -196,6 +196,8 @@ const char *xemu_settings_get_default_eeprom_path(void) bool xemu_settings_load(void) { + const int kMaxAudioVpWorkers = 16; + xemu_settings_apply_defaults(); error_msg.clear(); setenv("XEMU_ANDROID_FORCE_CPU_BLIT", "0", 1); @@ -219,6 +221,8 @@ bool xemu_settings_load(void) auto general = tbl["general"]; auto display = tbl["display"]; auto display_window = display["window"]; + auto audio = tbl["audio"]; + auto audio_vp = audio["vp"]; auto perf = tbl["perf"]; auto android_cfg = tbl["android"]; auto sys = tbl["sys"]; @@ -259,6 +263,32 @@ bool xemu_settings_load(void) g_config.perf.cache_shaders = *cache_shaders; } + // Audio settings + if (auto vp_workers = audio_vp["num_workers"].value()) { + int workers = (int)*vp_workers; + if (workers < 0) { + workers = 0; + } else if (workers > kMaxAudioVpWorkers) { + workers = kMaxAudioVpWorkers; + } + g_config.audio.vp.num_workers = workers; + } + if (auto use_dsp = audio["use_dsp"].value()) { + g_config.audio.use_dsp = *use_dsp; + } + if (auto hrtf = audio["hrtf"].value()) { + g_config.audio.hrtf = *hrtf; + } + if (auto volume_limit = audio["volume_limit"].value()) { + double volume = *volume_limit; + if (volume < 0.0) { + volume = 0.0; + } else if (volume > 1.0) { + volume = 1.0; + } + g_config.audio.volume_limit = volume; + } + // Android-specific settings if (auto force_cpu_blit = android_cfg["force_cpu_blit"].value()) { setenv("XEMU_ANDROID_FORCE_CPU_BLIT", *force_cpu_blit ? "1" : "0", 1); @@ -291,6 +321,43 @@ bool xemu_settings_load(void) snprintf(tb_size_str, sizeof(tb_size_str), "%d", tb_size); setenv("XEMU_ANDROID_TCG_TB_SIZE", tb_size_str, 1); } + if (auto inline_aio = android_cfg["inline_aio"].value()) { + setenv("XEMU_ANDROID_INLINE_AIO", *inline_aio ? "1" : "0", 1); + } + if (auto vp_workers = android_cfg["vp_workers"].value()) { + int workers = (int)*vp_workers; + if (workers < 0) { + workers = 0; + } else if (workers > kMaxAudioVpWorkers) { + workers = kMaxAudioVpWorkers; + } + char workers_str[16]; + snprintf(workers_str, sizeof(workers_str), "%d", workers); + setenv("XEMU_ANDROID_VP_WORKERS", workers_str, 1); + } + if (auto audio_samples = android_cfg["audio_samples"].value()) { + int samples = (int)*audio_samples; + if (samples < 256) { + samples = 256; + } else if (samples > 4096) { + samples = 4096; + } + char samples_str[16]; + snprintf(samples_str, sizeof(samples_str), "%d", samples); + setenv("XEMU_ANDROID_AUDIO_SAMPLES", samples_str, 1); + } + if (auto audio_fifo_frames = + android_cfg["audio_fifo_frames"].value()) { + int fifo_frames = (int)*audio_fifo_frames; + if (fifo_frames < 3) { + fifo_frames = 3; + } else if (fifo_frames > 32) { + fifo_frames = 32; + } + char fifo_str[16]; + snprintf(fifo_str, sizeof(fifo_str), "%d", fifo_frames); + setenv("XEMU_ANDROID_AUDIO_FIFO_FRAMES", fifo_str, 1); + } // System file paths if (auto bootrom = sys_files["bootrom_path"].value()) { diff --git a/block/file-posix.c b/block/file-posix.c index 4a66e18976..45c457cff1 100644 --- a/block/file-posix.c +++ b/block/file-posix.c @@ -2529,13 +2529,12 @@ static int coroutine_fn raw_thread_pool_submit(ThreadPoolFunc func, void *arg) /* * Some Android devices crash in coroutine context switches used by * thread_pool_submit_co(). Allow forcing synchronous execution for - * raw I/O to avoid that path. Enabled by default on Android, set - * XEMU_ANDROID_INLINE_AIO=0 to restore thread-pool behavior. + * raw I/O to avoid that path when explicitly enabled. */ static int inline_aio = -1; if (inline_aio < 0) { const char *value = getenv("XEMU_ANDROID_INLINE_AIO"); - inline_aio = (!value || value[0] != '0') ? 1 : 0; + inline_aio = (value && value[0] != '0') ? 1 : 0; } if (inline_aio) { return func(arg); diff --git a/hw/xbox/mcpx/apu/apu.c b/hw/xbox/mcpx/apu/apu.c index c8ae00d42d..7013c25d1a 100644 --- a/hw/xbox/mcpx/apu/apu.c +++ b/hw/xbox/mcpx/apu/apu.c @@ -189,6 +189,29 @@ static void sleep_ns(int64_t ns) #endif } +static int getenv_int_clamped(const char *name, int min_value, int max_value, + int fallback) +{ + const char *value = getenv(name); + if (!value || value[0] == '\0') { + return fallback; + } + + char *end = NULL; + long parsed = strtol(value, &end, 10); + if (end == value || *end != '\0') { + return fallback; + } + + if (parsed < min_value) { + return min_value; + } + if (parsed > max_value) { + return max_value; + } + return (int)parsed; +} + static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b) { MCPXAPUState *s = MCPX_APU_DEVICE(opaque); @@ -199,29 +222,37 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b) } int avail = 0; - while (avail < free_b) { + for (int i = 0; i < 10; i++) { qemu_spin_lock(&s->monitor.fifo_lock); avail = fifo8_num_used(&s->monitor.fifo); qemu_spin_unlock(&s->monitor.fifo_lock); - if (avail < free_b) { - sleep_ns(1000000); - qemu_cond_broadcast(&s->cond); + if (avail >= free_b) { + break; } + sleep_ns(500000); + qemu_cond_broadcast(&s->cond); if (!runstate_is_running()) { memset(stream, 0, free_b); return; } } + int copied = 0; int to_copy = MIN(free_b, avail); - while (to_copy > 0) { + while (copied < to_copy) { uint32_t chunk_len = 0; qemu_spin_lock(&s->monitor.fifo_lock); - chunk_len = fifo8_pop_buf(&s->monitor.fifo, stream, to_copy); - assert(chunk_len <= to_copy); + chunk_len = fifo8_pop_buf(&s->monitor.fifo, stream + copied, + to_copy - copied); qemu_spin_unlock(&s->monitor.fifo_lock); - stream += chunk_len; - to_copy -= chunk_len; + if (!chunk_len) { + break; + } + copied += chunk_len; + } + + if (copied < free_b) { + memset(stream + copied, 0, free_b - copied); } qemu_cond_broadcast(&s->cond); @@ -230,13 +261,24 @@ 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); - fifo8_create(&d->monitor.fifo, 3 * (256 * 2 * 2)); + + int fifo_frames = 3; + int audio_samples = 512; +#ifdef __ANDROID__ + fifo_frames = 8; + audio_samples = 1024; + fifo_frames = getenv_int_clamped("XEMU_ANDROID_AUDIO_FIFO_FRAMES", 3, 32, + fifo_frames); + audio_samples = getenv_int_clamped("XEMU_ANDROID_AUDIO_SAMPLES", 256, 4096, + audio_samples); +#endif + fifo8_create(&d->monitor.fifo, fifo_frames * sizeof(d->monitor.frame_buf)); struct SDL_AudioSpec sdl_audio_spec = { .freq = 48000, .format = AUDIO_S16LSB, .channels = 2, - .samples = 512, + .samples = audio_samples, .callback = monitor_sink_cb, .userdata = d, }; diff --git a/hw/xbox/mcpx/apu/vp/vp.c b/hw/xbox/mcpx/apu/vp/vp.c index 195527052b..3e92835ad1 100644 --- a/hw/xbox/mcpx/apu/vp/vp.c +++ b/hw/xbox/mcpx/apu/vp/vp.c @@ -1757,11 +1757,48 @@ voice_work_dispatch(MCPXAPUState *d, qemu_mutex_unlock(&vwd->lock); } +static int mcpx_apu_default_vp_worker_count(void) +{ + int cpu_count = MAX(1, SDL_GetCPUCount()); + +#ifdef __ANDROID__ + /* + * Mobile SoCs are often oversubscribed already (TCG + render + I/O). + * Keep VP worker defaults conservative to reduce thread contention and + * audio underruns; allow explicit override via env. + */ + const char *value = getenv("XEMU_ANDROID_VP_WORKERS"); + if (value && value[0] != '\0') { + char *end = NULL; + long parsed = strtol(value, &end, 10); + if (end != value && *end == '\0' && parsed > 0) { + return (int)parsed; + } + } + + if (cpu_count <= 2) { + return 1; + } + if (cpu_count <= 4) { + return 2; + } + if (cpu_count <= 6) { + return 3; + } + return 4; +#else + return cpu_count; +#endif +} + static void voice_work_init(MCPXAPUState *d) { VoiceWorkDispatch *vwd = &d->vp.voice_work_dispatch; - int num_workers = g_config.audio.vp.num_workers ?: SDL_GetCPUCount(); + int num_workers = g_config.audio.vp.num_workers; + if (num_workers <= 0) { + num_workers = mcpx_apu_default_vp_worker_count(); + } vwd->num_workers = MAX(1, MIN(num_workers, MAX_VOICE_WORKERS)); vwd->workers = g_malloc0_n(vwd->num_workers, sizeof(VoiceWorker)); vwd->workers_should_exit = false;