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