@@ -277,39 +277,31 @@ public static void NormaliseFrameworkIniExecutionMode()
277277 }
278278 }
279279
280- // Sentinel file dropped after the one-shot Renderer-default migration has
281- // run. Stored in the storage root next to framework.ini so a single
282- // existence check governs whether we should respect the user's currently
283- // persisted Renderer choice (sentinel present) or perform the one-time
284- // Automatic→OpenGL nudge (sentinel absent).
280+ // Sentinel file dropped after the one-shot Android renderer-default
281+ // normalisation has run. Stored in the storage root next to framework.ini
282+ // so a single existence check governs whether startup should touch the
283+ // renderer default at all.
285284 private const string renderer_migration_sentinel = "android_renderer_default_migrated.flag" ;
286285
287286 /// <summary>
288- /// One-shot migration that flips the framework default <c>Renderer</c>
289- /// choice from <c>Automatic</c> (which resolves to Vulkan on Android,
290- /// requiring runtime SPIR-V compilation via glslang) to <c>OpenGL</c>
291- /// (which uses the Adreno driver's native GLSL compiler — no glslang,
292- /// no SPIR-V, and therefore no shader-compile burst on Toolbar load).
287+ /// One-shot Android renderer-default normalisation.
293288 ///
294289 /// <para>
295- /// Why this matters: every recent black-screen ANR fingerprint in the
296- /// field tombstones (PIDs 27798 / 29226 / 499) shows a Veldrid worker
297- /// stuck inside <c>glslang::TParseContext::executeInitializer</c> /
298- /// <c>TShader::parse</c> at <c>nice=-10</c> on a big core, monopolising
299- /// the CPU during the Toolbar texture-upload burst and starving the
300- /// Update thread past the 10-second MotionEvent ANR deadline. Switching
301- /// the default away from the Vulkan-via-glslang path eliminates the
302- /// entire failure class on stock installs. Users who specifically want
303- /// Vulkan can still select it from Settings → Graphics → Renderer; the
304- /// migration only nudges the *default* and is recorded by an on-disk
305- /// sentinel so subsequent launches never overwrite an explicit choice.
290+ /// Earlier builds rewrote the Android default renderer to <c>OpenGL</c>
291+ /// on first launch in an attempt to dodge a Vulkan startup hang.
292+ /// Current field logs show the opposite failure mode as well:
293+ /// some devices now black-screen before the first managed heartbeat while
294+ /// booting the OpenGL/ANGLE path. Rewriting the default in either
295+ /// direction is therefore too risky; the framework's own default should
296+ /// be left untouched and safe-mode should only intervene after an actual
297+ /// failed launch.
306298 /// </para>
307299 ///
308300 /// <para>
309- /// Best-effort and never throws — if the file is missing or the rewrite
310- /// fails, startup proceeds with the existing value. Must be invoked from
311- /// <c>OsuGameActivity.OnCreate</c> BEFORE the framework reads
312- /// framework.ini, alongside the existing
301+ /// Best-effort and never throws. The method now only drops its sentinel
302+ /// so future launches know the normalisation has already been considered.
303+ /// Must be invoked from <c>OsuGameActivity.OnCreate</c> BEFORE the
304+ /// framework reads framework .ini, alongside the existing
313305 /// <see cref="NormaliseFrameworkIniExecutionMode"/> hook.
314306 /// </para>
315307 /// </summary>
@@ -323,95 +315,6 @@ public static void NormaliseFrameworkIniRendererDefault()
323315 string sentinelPath = Path . Combine ( root , renderer_migration_sentinel ) ;
324316 if ( File . Exists ( sentinelPath ) ) return ;
325317
326- string iniPath = Path . Combine ( root , "framework.ini" ) ;
327-
328- if ( ! File . Exists ( iniPath ) )
329- {
330- // Brand-new install: no framework.ini yet. Pre-create a minimal
331- // file with just the Renderer line set; the framework will fill
332- // in its other defaults on first save.
333- try
334- {
335- File . WriteAllText ( iniPath , "Renderer = OpenGL" + System . Environment . NewLine ) ;
336- tryDropSentinel ( sentinelPath ) ;
337- }
338- catch ( Exception e )
339- {
340- Debug . WriteLine ( $ "[osu!] LogManagement: could not pre-create framework.ini: { e . Message } ") ;
341- }
342- return ;
343- }
344-
345- string [ ] lines ;
346-
347- try
348- {
349- lines = File . ReadAllLines ( iniPath ) ;
350- }
351- catch ( Exception e )
352- {
353- Debug . WriteLine ( $ "[osu!] LogManagement: could not read framework.ini for renderer migration: { e . Message } ") ;
354- return ;
355- }
356-
357- bool changed = false ;
358- bool seenRendererLine = false ;
359-
360- for ( int i = 0 ; i < lines . Length ; i ++ )
361- {
362- string line = lines [ i ] ;
363- int eq = line . IndexOf ( '=' ) ;
364- if ( eq <= 0 ) continue ;
365-
366- string key = line . Substring ( 0 , eq ) . Trim ( ) ;
367- string value = line . Substring ( eq + 1 ) . Trim ( ) ;
368-
369- if ( ! string . Equals ( key , "Renderer" , StringComparison . Ordinal ) )
370- continue ;
371-
372- seenRendererLine = true ;
373-
374- // Only nudge the default. If the user has explicitly chosen
375- // Vulkan / OpenGLLegacy / Direct3D11 / Metal / Deferred, leave
376- // it alone — the migration's job is to change the *default*,
377- // not overwrite intent.
378- if ( string . Equals ( value , "Automatic" , StringComparison . Ordinal ) )
379- {
380- lines [ i ] = "Renderer = OpenGL" ;
381- changed = true ;
382- }
383-
384- break ;
385- }
386-
387- if ( ! seenRendererLine )
388- {
389- // No Renderer line at all — append one at the end of the file.
390- var newLines = new string [ lines . Length + 1 ] ;
391- Array . Copy ( lines , newLines , lines . Length ) ;
392- newLines [ lines . Length ] = "Renderer = OpenGL" ;
393- lines = newLines ;
394- changed = true ;
395- }
396-
397- if ( changed )
398- {
399- try
400- {
401- File . WriteAllLines ( iniPath , lines ) ;
402- Logger . Log ( "[osu!] Android first-launch Renderer-default migration: Automatic → OpenGL" , LoggingTarget . Performance ) ;
403- }
404- catch ( Exception e )
405- {
406- Debug . WriteLine ( $ "[osu!] LogManagement: could not rewrite framework.ini for renderer migration: { e . Message } ") ;
407- return ;
408- }
409- }
410-
411- // Drop sentinel regardless of whether we changed anything: the
412- // migration has now had its one chance to run, and any subsequent
413- // user choice (including a deliberate "Automatic") must be
414- // respected.
415318 tryDropSentinel ( sentinelPath ) ;
416319 }
417320 catch ( Exception e )
@@ -435,35 +338,33 @@ private static void tryDropSentinel(string sentinelPath)
435338 /// <summary>
436339 /// Safe-mode renderer fallback: when the previous launch died before reaching the
437340 /// post-LoadComplete clear point (i.e. <see cref="AndroidStartupSafeMode.IsActive"/>
438- /// is true), force <c>Renderer = OpenGL</c> in the on-disk <c>framework.ini </c>
341+ /// is true), switch the on-disk <c>Renderer </c> away from the previous startup path
439342 /// for this launch only.
440343 ///
441344 /// <para>
442- /// Why: the recurring black-screen ANR fingerprint is a Vulkan-path Toolbar-time
443- /// stall on Adreno (driver MAILBOX deadlock + glslang shader-compile burst),
444- /// and the failure has been reproduced across multiple Adreno generations
445- /// (high-end 740 and a lower-end model with Vulkan < 1.3) — i.e. it is a
446- /// cross-driver issue, not a single-device quirk. If the user has explicitly
447- /// picked Vulkan and the previous launch died inside it, re-attempting Vulkan
448- /// immediately reproduces the same hang.
345+ /// Why: older builds primarily failed on the Vulkan startup path, so safe-mode
346+ /// forced <c>OpenGL</c>. Current crash logs also show devices that wedge before
347+ /// the first managed heartbeat on the OpenGL/ANGLE path. Safe-mode therefore
348+ /// needs to escape whichever renderer was persisted previously instead of always
349+ /// retrying the same one.
449350 /// </para>
450351 ///
451352 /// <para>
452- /// Persistence: the original renderer value is saved to
353+ /// Persistence: when safe-mode switches <em>to</em> <c>OpenGL</c>, the original
354+ /// renderer value is saved to
453355 /// <see cref="AndroidStartupFlags.FLAG_SAFE_MODE_RENDERER_RESTORE"/> before
454356 /// being overwritten. <see cref="RestoreRendererAfterSafeMode"/> reads this on
455357 /// the next successful launch and restores the renderer automatically — making
456- /// this a single-launch rescue rather than a permanent override. Users who
457- /// deliberately want to stay on OpenGL after a crash can change the setting
458- /// themselves; users who have Vulkan working correctly are automatically returned
459- /// to it on the next clean start.
358+ /// Vulkan→OpenGL a single-launch rescue rather than a permanent override.
359+ /// OpenGL→Automatic fallbacks deliberately do <em>not</em> auto-restore, because
360+ /// restoring the same failing OpenGL path would recreate the startup loop.
460361 /// </para>
461362 ///
462363 /// <para>
463364 /// Bypasses the <c>renderer_migration_sentinel</c> deliberately —
464365 /// <see cref="NormaliseFrameworkIniRendererDefault"/> is one-shot and intentionally
465366 /// respects user intent on subsequent launches; this method's job is precisely the
466- /// opposite (override user intent when their previous Vulkan launch died ).
367+ /// opposite (override user intent when their previous startup path just failed ).
467368 /// </para>
468369 ///
469370 /// <para>
@@ -491,26 +392,26 @@ public static void ForceOpenGLRendererIfSafeMode()
491392 // • Partially-written or incomplete pipeline objects from the
492393 // interrupted Vulkan compile pass.
493394 //
494- // Either case causes visual corruption on the rescue OpenGL session:
395+ // Either case causes visual corruption on the rescue renderer session:
495396 // – Argon hit circles render as white rectangles (masking uniform
496397 // at wrong struct offset → CornerRadius clipping broken).
497398 // – TrianglesV2 buttons show the wrong hue (gradient colour data
498399 // at wrong offset → DrawColourInfo.Colour.Interpolate returns
499400 // garbage channel values).
500401 //
501402 // Wipe the shader cache unconditionally here — bypassing the
502- // version-code sentinel — so the OpenGL rescue session always starts
503- // from a clean slate. The sentinel is NOT reset: the next normal
403+ // version-code sentinel — so the rescue renderer always starts
404+ // from a clean slate. The sentinel is NOT reset: the next normal
504405 // (non-safe-mode) launch will still skip the version wipe and reuse
505- // the freshly-compiled OpenGL cache from this rescue session.
406+ // whatever cache the successful rescue session just rebuilt .
506407 string shaderCacheDir = Path . Combine ( root , "cache" , "shaders" ) ;
507408
508409 if ( Directory . Exists ( shaderCacheDir ) )
509410 {
510411 try
511412 {
512413 Directory . Delete ( shaderCacheDir , recursive : true ) ;
513- Logger . Log ( "[osu!] Android safe-mode: shader cache wiped to ensure clean OpenGL recompilation." , LoggingTarget . Runtime ) ;
414+ Logger . Log ( "[osu!] Android safe-mode: shader cache wiped to ensure a clean renderer fallback recompilation." , LoggingTarget . Runtime ) ;
514415 }
515416 catch ( Exception e )
516417 {
@@ -528,8 +429,9 @@ public static void ForceOpenGLRendererIfSafeMode()
528429 // safe renderer choice so the framework picks it up on first read.
529430 try
530431 {
531- File . WriteAllText ( iniPath , "Renderer = OpenGL" + System . Environment . NewLine ) ;
532- Logger . Log ( "[osu!] Android safe-mode renderer fallback: pre-created framework.ini with Renderer = OpenGL" , LoggingTarget . Performance ) ;
432+ const string fallbackRenderer = "OpenGL" ;
433+ File . WriteAllText ( iniPath , $ "Renderer = { fallbackRenderer } " + System . Environment . NewLine ) ;
434+ Logger . Log ( $ "[osu!] Android safe-mode renderer fallback: pre-created framework.ini with Renderer = { fallbackRenderer } ", LoggingTarget . Performance ) ;
533435 }
534436 catch ( Exception e )
535437 {
@@ -553,6 +455,7 @@ public static void ForceOpenGLRendererIfSafeMode()
553455 bool changed = false ;
554456 bool seenRendererLine = false ;
555457 string ? previousValue = null ;
458+ string fallbackRenderer = "OpenGL" ;
556459
557460 for ( int i = 0 ; i < lines . Length ; i ++ )
558461 {
@@ -568,10 +471,11 @@ public static void ForceOpenGLRendererIfSafeMode()
568471
569472 seenRendererLine = true ;
570473 previousValue = value ;
474+ fallbackRenderer = chooseSafeModeRenderer ( previousValue ) ;
571475
572- if ( ! string . Equals ( value , "OpenGL" , StringComparison . Ordinal ) )
476+ if ( ! string . Equals ( value , fallbackRenderer , StringComparison . Ordinal ) )
573477 {
574- lines [ i ] = "Renderer = OpenGL " ;
478+ lines [ i ] = $ "Renderer = { fallbackRenderer } ";
575479 changed = true ;
576480 }
577481
@@ -582,7 +486,7 @@ public static void ForceOpenGLRendererIfSafeMode()
582486 {
583487 var newLines = new string [ lines . Length + 1 ] ;
584488 Array . Copy ( lines , newLines , lines . Length ) ;
585- newLines [ lines . Length ] = "Renderer = OpenGL " ;
489+ newLines [ lines . Length ] = $ "Renderer = { fallbackRenderer } ";
586490 lines = newLines ;
587491 changed = true ;
588492 }
@@ -597,7 +501,9 @@ public static void ForceOpenGLRendererIfSafeMode()
597501 // "OpenGL" from the previous safe-mode write.
598502 string ? existingRestore = AndroidStartupFlags . ReadValue ( AndroidStartupFlags . FLAG_SAFE_MODE_RENDERER_RESTORE ) ;
599503
600- if ( existingRestore == null && previousValue != null
504+ if ( existingRestore == null
505+ && previousValue != null
506+ && string . Equals ( fallbackRenderer , "OpenGL" , StringComparison . Ordinal )
601507 && ! string . Equals ( previousValue , "OpenGL" , StringComparison . Ordinal ) )
602508 {
603509 AndroidStartupFlags . WriteValue ( AndroidStartupFlags . FLAG_SAFE_MODE_RENDERER_RESTORE , previousValue ) ;
@@ -609,7 +515,10 @@ public static void ForceOpenGLRendererIfSafeMode()
609515 string reason = AndroidStartupSafeMode . DrawThreadNativeCrashTriggered
610516 ? "Draw-thread native crash detected"
611517 : "previous launch died before LoadComplete clear point" ;
612- Logger . Log ( $ "[osu!] Android safe-mode renderer fallback ({ reason } ): Renderer { previousValue ?? "(unset)" } → OpenGL (temporary; will restore to { previousValue } after next successful launch)", LoggingTarget . Performance ) ;
518+ string restoreText = string . Equals ( fallbackRenderer , "OpenGL" , StringComparison . Ordinal ) && previousValue != null
519+ ? $ "temporary; will restore to { previousValue } after next successful launch"
520+ : "sticky until the user changes it again" ;
521+ Logger . Log ( $ "[osu!] Android safe-mode renderer fallback ({ reason } ): Renderer { previousValue ?? "(unset)" } → { fallbackRenderer } ({ restoreText } )", LoggingTarget . Performance ) ;
613522 }
614523 catch ( Exception e )
615524 {
@@ -622,6 +531,13 @@ public static void ForceOpenGLRendererIfSafeMode()
622531 }
623532 }
624533
534+ private static string chooseSafeModeRenderer ( string ? previousRenderer )
535+ {
536+ return string . Equals ( previousRenderer , "OpenGL" , StringComparison . OrdinalIgnoreCase )
537+ ? "Automatic"
538+ : "OpenGL" ;
539+ }
540+
625541 /// <summary>
626542 /// Called from <see cref="AndroidStartupSafeMode.ClearStartupInProgress"/> once the
627543 /// current launch is healthy. If <see cref="AndroidStartupFlags.FLAG_SAFE_MODE_RENDERER_RESTORE"/>
0 commit comments