From 01cb23d4168cf3062cfb08040940471569b8aa4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:13:54 +0000 Subject: [PATCH 1/3] Skip redundant setRequestedOrientation calls to prevent ANR Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/b41f047d-4494-4c84-93a7-7854e96c9a67 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameAndroid.cs | 56 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 81027e89f927..645cf11df115 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -88,6 +88,14 @@ public partial class OsuGameAndroid : OsuGame private object? activeMixersList; private object? nativeBridges; + + /// + /// Last value passed to by + /// . Cached locally so we can short-circuit + /// redundant updates without round-tripping through the activity getter, which + /// itself performs a binder IPC on modern Android. + /// + private global::Android.Content.PM.ScreenOrientation? lastRequestedOrientation; private int currentRefreshRate; public OsuGameAndroid(OsuGameActivity activity) @@ -753,24 +761,44 @@ private void updateOrientation() var orientation = MobileUtils.GetOrientation(this, currentScreen, gameActivity.IsTablet); + global::Android.Content.PM.ScreenOrientation desired; + + switch (orientation) + { + case MobileUtils.Orientation.Locked: + desired = global::Android.Content.PM.ScreenOrientation.Locked; + break; + + case MobileUtils.Orientation.Portrait: + desired = global::Android.Content.PM.ScreenOrientation.Portrait; + break; + + case MobileUtils.Orientation.Default: + desired = gameActivity.DefaultOrientation; + break; + + default: + return; + } + + // Short-circuit when no change is required. We track the last requested orientation + // locally because Activity.getRequestedOrientation() itself performs a binder IPC + // on modern Android, and the whole point of this guard is to avoid binder traffic. + // ScreenChanged fires on every screen push/pop and the resolved orientation rarely + // differs between adjacent screens, so without this guard we flood the UI looper + // with redundant Activity.setRequestedOrientation transactions, which under + // system_server CPU pressure can wedge input dispatch and trigger an ANR + // ("Input dispatching timed out ... Waited 10000ms for MotionEvent"). + if (lastRequestedOrientation == desired) + return; + + lastRequestedOrientation = desired; + gameActivity.RunOnUiThread(() => { try { - switch (orientation) - { - case MobileUtils.Orientation.Locked: - gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Locked; - break; - - case MobileUtils.Orientation.Portrait: - gameActivity.RequestedOrientation = global::Android.Content.PM.ScreenOrientation.Portrait; - break; - - case MobileUtils.Orientation.Default: - gameActivity.RequestedOrientation = gameActivity.DefaultOrientation; - break; - } + gameActivity.RequestedOrientation = desired; } catch (Exception e) { From 181e6adac6ce3a893cb6a309d1dd0f0d99168ebe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:22:27 +0000 Subject: [PATCH 2/3] Throttle FirstChanceException disk floods on Draw thread Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/c49ade03-b6dc-45b5-81e0-3c6d779f6e31 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/CrashDiagnostics.cs | 40 +++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/osu.Android/CrashDiagnostics.cs b/osu.Android/CrashDiagnostics.cs index 3e8ce28d976b..db5d393012bf 100644 --- a/osu.Android/CrashDiagnostics.cs +++ b/osu.Android/CrashDiagnostics.cs @@ -37,9 +37,25 @@ internal static class CrashDiagnostics private static int initialised; private static int managedHooksInstalled; + // Global cap on FirstChanceException dumps written per process. A hot-path + // throw loop (e.g. Veldrid "surface lost" thrown every Draw frame while the + // Android Vulkan surface is unavailable during a slow startup) can otherwise + // produce hundreds of full-stack dumps, each one a synchronous file write + // on the throwing thread — which itself stalls the Draw thread and worsens + // the very condition causing the throws. + private const int first_chance_global_cap = 50; + + // Per-unique-stack cap. Higher (10) for true fatal kinds caught via + // FirstChanceException-fallback or AppDomain.UnhandledException; lower (3) + // for first-chance noise where seeing the first few occurrences is enough + // to diagnose and the rest are pure log bloat. + private const int per_key_cap_default = 10; + private const int per_key_cap_first_chance = 3; + private static string? internalDir; private static string? externalDir; private static readonly ConcurrentDictionary exceptionCounts = new ConcurrentDictionary(); + private static int firstChanceWriteCount; private static string? sentinelPath; private static string? installedLogPath; private static bool sentinelWritten; @@ -290,8 +306,18 @@ private static void writeManagedException(string source, Exception? ex) if (ex is EntryPointNotFoundException && ex.Message.Contains("CFStringCreateWithCharacters")) return; + bool isFirstChance = source.StartsWith("FirstChanceException", StringComparison.Ordinal); + + // Global cap on first-chance dumps: a hot-path throw loop on the Draw + // thread can otherwise produce unbounded synchronous file writes, which + // themselves stall the Draw thread and worsen the surface-acquisition + // problem that caused the throws. + if (isFirstChance && Interlocked.Increment(ref firstChanceWriteCount) > first_chance_global_cap) + return; + + int perKeyCap = isFirstChance ? per_key_cap_first_chance : per_key_cap_default; string key = $"{source}_{ex?.GetType().Name}_{ex?.StackTrace?.GetHashCode() ?? 0}"; - if (exceptionCounts.AddOrUpdate(key, 1, (_, count) => count + 1) > 10) + if (exceptionCounts.AddOrUpdate(key, 1, (_, count) => count + 1) > perKeyCap) return; try @@ -306,7 +332,17 @@ private static void writeManagedException(string source, Exception? ex) "\n" + (ex?.ToString() ?? "") + "\n" + "=== END OF MANAGED EXCEPTION ===\n\n"; - appendToBoth(block); + + // For FirstChanceException we deliberately skip the external/FUSE + // write — those writes are tens of milliseconds each and run on the + // throwing thread (often the Draw thread). MirrorInternalLogToExternal + // copies the internal log to external on the next startup, which is + // sufficient for user-facing diagnostics without risking a Draw-thread + // stall in the live process. + if (isFirstChance) + tryAppend(internalDir, block); + else + appendToBoth(block); } catch (Exception e) { From 4c2868a46ec772f85201b2a071efe312b221386c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:42:45 +0000 Subject: [PATCH 3/3] Remove redundant RequestedOrientation write in OnCreate that triggers SurfaceView recreate during Vulkan-init Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/0e3fd697-aaa5-41a9-938f-942bcf040a08 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/OsuGameActivity.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 2d40012a7989..483341514097 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -83,8 +83,19 @@ public OsuGameActivity() protected override void OnCreate(Bundle? savedInstanceState) { - // Force orientation immediately to prevent unnecessary surface recreation on startup. - RequestedOrientation = ScreenOrientation.Landscape; + // NOTE: do NOT assign RequestedOrientation here. The `[Activity]` attribute on this + // class already declares `ScreenOrientation = ScreenOrientation.Landscape`, so the + // activity is created in landscape from the very first frame. A runtime re-assignment + // *before* `base.OnCreate` (which is where SDL constructs the SurfaceView) gets + // queued by Android and delivered exactly during initial SurfaceView setup, nudging + // the SurfaceView into a destroy/recreate cycle on some OEMs while the SDL draw + // thread is mid-Vulkan-init. The framework's `VeldridDevice` then either times out + // its 5s `SurfaceHandle` poll (constructor throws, renderer never comes up) or hands + // a stale handle to `vkCreateAndroidSurfaceKHR` (driver SIGSEGV) — either way the + // game never renders a frame and the user is left staring at a black screen while + // the per-frame retry / FirstChanceException pipeline floods the log. The same + // invariant is documented further down in this method (see the "Phones: manifest + // already requests Landscape; do not re-assign at runtime" block). // Crash diagnostics first. The native handler write target is internal storage // (FilesDir/native_crash.log); a one-shot mirror copies it to external storage