Skip to content

Commit 8ec950f

Browse files
Fix Android startup renderer fallback loop
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/1fee72f1-9316-43b1-b7bc-065549cee143 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 53d0ffb commit 8ec950f

5 files changed

Lines changed: 70 additions & 155 deletions

File tree

osu.Android/AndroidStartupFlags.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ internal static class AndroidStartupFlags
7777
public const string FLAG_STARTUP_IN_PROGRESS = "android_startup_in_progress.flag";
7878

7979
/// <summary>
80-
/// Stores the renderer value that was overwritten by <see cref="LogManagement.ForceOpenGLRendererIfSafeMode"/>.
80+
/// Stores the renderer value that was overwritten by <see cref="LogManagement.ForceOpenGLRendererIfSafeMode"/>
81+
/// when safe-mode temporarily switches the app to OpenGL.
8182
/// On the next successful launch, <see cref="LogManagement.RestoreRendererAfterSafeMode"/> reads this,
8283
/// restores the renderer in <c>framework.ini</c>, and deletes the file so the restore only happens once.
83-
/// This makes the safe-mode OpenGL fallback a single-launch rescue rather than a permanent override.
8484
/// </summary>
8585
public const string FLAG_SAFE_MODE_RENDERER_RESTORE = "android_safe_mode_renderer_restore.flag";
8686

osu.Android/AndroidStartupSafeMode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private static void applyIfPreviousDrawThreadNativeCrash()
159159
+ $" thread_name = {crash.Value.ThreadName}\n"
160160
+ $" top_frame = {crash.Value.TopFrame}\n"
161161
+ $" fingerprint = {fingerprint}\n"
162-
+ " effects = defer Oboe/Vulkan-probe init; skip FrameSync migration; longer refresh-rate defer; force Renderer = OpenGL\n"
162+
+ " effects = defer Oboe/Vulkan-probe init; skip FrameSync migration; longer refresh-rate defer; apply renderer fallback\n"
163163
+ "=== END SAFE-MODE BANNER ===\n\n");
164164
}
165165
catch (Exception e)

osu.Android/LogManagement.cs

Lines changed: 56 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -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 &lt; 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

Comments
 (0)