@@ -199,13 +199,24 @@ public static void InstallManagedExceptionHooks()
199199 // AppDomain.UnhandledException before aborting — so without this hook the
200200 // exception that ultimately kills the process can vanish without trace. We
201201 // record it here on every throw so the *last* recorded exception before a
202- // SIGSEGV/SIGABRT is the candidate culprit. To avoid drowning the log in noise
203- // we filter by exception type — only fatal-ish kinds are recorded.
202+ // SIGSEGV/SIGABRT is the candidate culprit.
203+ //
204+ // Filtering policy:
205+ // * On osu.Framework GameThreads (Draw/Update/Audio/Input): log *every*
206+ // exception. An unhandled throw on any of these threads will tear down
207+ // the process via Mono's tgkill(SIGSEGV) path with no managed trace
208+ // reaching AppDomain.UnhandledException, so we cannot afford to filter.
209+ // * On all other threads: keep the legacy "fatal-ish kinds" type filter
210+ // so the log is not flooded by routine first-chance noise (e.g. the
211+ // HidSharp / CFStringCreateWithCharacters EntryPointNotFoundException
212+ // that fires every startup on .NET TP Worker).
204213 try
205214 {
206215 AppDomain . CurrentDomain . FirstChanceException += ( _ , e ) =>
207216 {
208- if ( e . Exception is NullReferenceException
217+ bool isGameThread = isOsuGameThread ( Thread . CurrentThread . Name ) ;
218+
219+ bool isFatalKind = e . Exception is NullReferenceException
209220 or AccessViolationException
210221 or StackOverflowException
211222 or TypeInitializationException
@@ -215,7 +226,9 @@ or BadImageFormatException
215226 or TypeLoadException
216227 or MissingMethodException
217228 or MissingFieldException
218- or InvalidProgramException )
229+ or InvalidProgramException ;
230+
231+ if ( isGameThread || isFatalKind )
219232 {
220233 writeManagedException ( $ "FirstChanceException ({ e . Exception . GetType ( ) . Name } )", e . Exception ) ;
221234 }
@@ -227,6 +240,23 @@ or MissingFieldException
227240 }
228241 }
229242
243+ // osu.Framework names its game threads with stable prefixes such as
244+ // "DrawThread", "UpdateThread", "AudioThread", "InputThread", and the
245+ // tombstone we are debugging shows the comm name "Draw (GameThread)".
246+ // Match any of these so an exception thrown deep inside the renderer or
247+ // audio pipeline gets captured before Mono aborts the process.
248+ private static bool isOsuGameThread ( string ? name )
249+ {
250+ if ( string . IsNullOrEmpty ( name ) )
251+ return false ;
252+
253+ return name . StartsWith ( "Draw" , StringComparison . Ordinal )
254+ || name . StartsWith ( "Update" , StringComparison . Ordinal )
255+ || name . StartsWith ( "Audio" , StringComparison . Ordinal )
256+ || name . StartsWith ( "Input" , StringComparison . Ordinal )
257+ || name . Contains ( "GameThread" , StringComparison . Ordinal ) ;
258+ }
259+
230260 /// <summary>
231261 /// Records a one-line summary of the native handler install state (sentinel exists?
232262 /// log path?) so the very first thing we see in the log on the next inspection tells
0 commit comments