From d809cba68d931d68b27bc0ba9534634cac7b35db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:35:01 +0000 Subject: [PATCH] Log every first-chance exception thrown on osu.Framework GameThreads Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/2e0e3d1e-4954-4a75-be3c-233040b818ef Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com> --- osu.Android/CrashDiagnostics.cs | 38 +++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Android/CrashDiagnostics.cs b/osu.Android/CrashDiagnostics.cs index 65587a30d7ba..4abf6f0f9111 100644 --- a/osu.Android/CrashDiagnostics.cs +++ b/osu.Android/CrashDiagnostics.cs @@ -199,13 +199,24 @@ public static void InstallManagedExceptionHooks() // AppDomain.UnhandledException before aborting — so without this hook the // exception that ultimately kills the process can vanish without trace. We // record it here on every throw so the *last* recorded exception before a - // SIGSEGV/SIGABRT is the candidate culprit. To avoid drowning the log in noise - // we filter by exception type — only fatal-ish kinds are recorded. + // SIGSEGV/SIGABRT is the candidate culprit. + // + // Filtering policy: + // * On osu.Framework GameThreads (Draw/Update/Audio/Input): log *every* + // exception. An unhandled throw on any of these threads will tear down + // the process via Mono's tgkill(SIGSEGV) path with no managed trace + // reaching AppDomain.UnhandledException, so we cannot afford to filter. + // * On all other threads: keep the legacy "fatal-ish kinds" type filter + // so the log is not flooded by routine first-chance noise (e.g. the + // HidSharp / CFStringCreateWithCharacters EntryPointNotFoundException + // that fires every startup on .NET TP Worker). try { AppDomain.CurrentDomain.FirstChanceException += (_, e) => { - if (e.Exception is NullReferenceException + bool isGameThread = isOsuGameThread(Thread.CurrentThread.Name); + + bool isFatalKind = e.Exception is NullReferenceException or AccessViolationException or StackOverflowException or TypeInitializationException @@ -215,7 +226,9 @@ or BadImageFormatException or TypeLoadException or MissingMethodException or MissingFieldException - or InvalidProgramException) + or InvalidProgramException; + + if (isGameThread || isFatalKind) { writeManagedException($"FirstChanceException ({e.Exception.GetType().Name})", e.Exception); } @@ -227,6 +240,23 @@ or MissingFieldException } } + // osu.Framework names its game threads with stable prefixes such as + // "DrawThread", "UpdateThread", "AudioThread", "InputThread", and the + // tombstone we are debugging shows the comm name "Draw (GameThread)". + // Match any of these so an exception thrown deep inside the renderer or + // audio pipeline gets captured before Mono aborts the process. + private static bool isOsuGameThread(string? name) + { + if (string.IsNullOrEmpty(name)) + return false; + + return name.StartsWith("Draw", StringComparison.Ordinal) + || name.StartsWith("Update", StringComparison.Ordinal) + || name.StartsWith("Audio", StringComparison.Ordinal) + || name.StartsWith("Input", StringComparison.Ordinal) + || name.Contains("GameThread", StringComparison.Ordinal); + } + /// /// Records a one-line summary of the native handler install state (sentinel exists? /// log path?) so the very first thing we see in the log on the next inspection tells