Skip to content

Commit e0bb789

Browse files
authored
Merge pull request #258 from winnerspiros/copilot/fix-black-screen-issue-da559b2d-b01a-4cb4-8285-f90896ea5463
Android: defer all remaining cold-start actions past the Toolbar texture-upload burst
2 parents 49d2280 + 6140c84 commit e0bb789

1 file changed

Lines changed: 112 additions & 51 deletions

File tree

osu.Android/OsuGameAndroid.cs

Lines changed: 112 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -367,31 +367,21 @@ protected override void LoadComplete()
367367
Logger.Log($"[osu!] Failed to pin threads: {e.Message}", LoggingTarget.Performance);
368368
}
369369

370-
// Always enable sustained performance mode for consistent frame delivery.
371-
// This prevents thermal throttling from causing sudden FPS drops.
372-
//
373-
// This MUST run on the Android UI thread: SetSustainedPerformanceMode mutates
374-
// window state through ViewRootImpl, which enforces single-threaded access via
375-
// checkThread() and throws CalledFromWrongThreadException otherwise. On some
376-
// OEM frameworks (observed on Samsung One UI / Adreno) that exception unwinds
377-
// through setPrivateFlags after the underlying Surface has already been
378-
// partially reconfigured, invalidating the active VkSurfaceKHR and crashing the
379-
// Vulkan driver inside vkCmdBeginRendering on the next frame.
380-
try
381-
{
382-
gameActivity.RunOnUiThread(() =>
383-
{
384-
try { gameActivity.Window?.SetSustainedPerformanceMode(true); }
385-
catch (Exception e)
386-
{
387-
Debug.WriteLine($"[osu!] Failed to enable sustained performance mode: {e.Message}");
388-
}
389-
});
390-
}
391-
catch (Exception e)
392-
{
393-
Debug.WriteLine($"[osu!] Failed to dispatch sustained performance mode toggle to UI thread: {e.Message}");
394-
}
370+
// Sustained performance mode is applied LATER, together with the deferred
371+
// display-mode / GC-latency work below. See the Scheduler.AddDelayed block
372+
// further down (after base.LoadComplete()) that schedules the first apply
373+
// on a refreshRateDelayMs timer. Running
374+
// Window.SetSustainedPerformanceMode(true) synchronously here — during the
375+
// Toolbar cold-start texture-upload burst and the Vulkan swapchain bring-up —
376+
// has been observed to race the Draw thread on Samsung One UI / Adreno panels:
377+
// the window-flag mutation round-trips through ViewRootImpl.setPrivateFlags
378+
// and can partially reconfigure the Surface while vkAcquireNextImageKHR is in
379+
// flight, stalling the present queue. Update keeps ticking (so neither the
380+
// managed nor the native watchdog ever dumps), the screen never updates, and
381+
// ~10 s later Android raises a MotionEvent input-dispatch ANR — the exact
382+
// cold-start "black screen → no touch → ANR" fingerprint reported across
383+
// multiple v174 launches in logs.zip. Deferring to the same window used by
384+
// SelectHighestRefreshRate moves the mutation behind the texture-upload burst.
395385

396386
base.LoadComplete();
397387

@@ -433,6 +423,78 @@ protected override void LoadComplete()
433423
{
434424
Debug.WriteLine($"[osu!] Deferred SelectHighestRefreshRate failed: {ex.Message}");
435425
}
426+
427+
// Deferred sustained-performance-mode apply. See the comment block
428+
// before base.LoadComplete() above for the rationale (Samsung One UI /
429+
// Adreno Surface reconfigure race with vkAcquireNextImageKHR during the
430+
// cold-start texture-upload burst). By the time this fires the
431+
// swapchain has long since stabilised.
432+
try
433+
{
434+
gameActivity.RunOnUiThread(() =>
435+
{
436+
try { gameActivity.Window?.SetSustainedPerformanceMode(true); }
437+
catch (Exception e)
438+
{
439+
Debug.WriteLine($"[osu!] Failed to enable sustained performance mode: {e.Message}");
440+
}
441+
});
442+
}
443+
catch (Exception e)
444+
{
445+
Debug.WriteLine($"[osu!] Failed to dispatch sustained performance mode toggle to UI thread: {e.Message}");
446+
}
447+
448+
// Deferred initial application of the user's performance-mode setting.
449+
// The BindValueChanged registration below is WITHOUT the immediate-fire
450+
// flag, so the very first apply (which may flip GCSettings.LatencyMode
451+
// to SustainedLowLatency via AndroidHighPerformanceSessionManager) is
452+
// done here, after the Toolbar texture-upload burst has drained. Running
453+
// it synchronously during LoadComplete suppresses gen-2 GCs while the
454+
// Draw thread is churning through hundreds of queued texture uploads,
455+
// causing the managed heap to balloon, the kernel to start paging
456+
// (VmSwap ~22 MB / RSS ~695 MB / memory-pressure avg10=1.34 observed in
457+
// the ANR dump), the Draw thread to stall on a page-fault burst, and
458+
// the main thread to miss its input-channel ACK deadline — another
459+
// contributor to the MotionEvent ANR fingerprint.
460+
try
461+
{
462+
applyPerformanceOptimizations(performanceMode.Value);
463+
}
464+
catch (Exception ex)
465+
{
466+
Debug.WriteLine($"[osu!] Deferred initial performance-mode apply failed: {ex.Message}");
467+
}
468+
469+
// Deferred UI-thread RequestUnbufferedDispatch(sources). Moved here from
470+
// the bottom of LoadComplete so the DecorView attribute mutation no
471+
// longer races the cold-start Toolbar texture-upload burst. OnCreate
472+
// already requested unbuffered dispatch once (with a dummy MotionEvent),
473+
// and every per-pointer DispatchTouchEvent / DispatchGenericMotionEvent
474+
// re-requests it as needed, so this global set-sources call is only a
475+
// latency polish for the first few real touches after the burst — it
476+
// brings no benefit during the black-screen window but does take a
477+
// binder IPC round-trip through ViewRootImpl, which we do not want
478+
// competing with swapchain settle work.
479+
try
480+
{
481+
gameActivity.RunOnUiThread(() =>
482+
{
483+
try
484+
{
485+
int sources = (int)(InputSourceType.Touchscreen | InputSourceType.Stylus | InputSourceType.Mouse | InputSourceType.Touchpad);
486+
gameActivity.Window?.DecorView?.RequestUnbufferedDispatch(sources);
487+
}
488+
catch (Exception e)
489+
{
490+
Debug.WriteLine($"[osu!] Failed to request unbuffered touch dispatch: {e.Message}");
491+
}
492+
});
493+
}
494+
catch (Exception e)
495+
{
496+
Debug.WriteLine($"[osu!] Failed to dispatch unbuffered-dispatch request to UI thread: {e.Message}");
497+
}
436498
}, refreshRateDelayMs);
437499

438500
// Clear the "startup in progress" sentinel once the current launch has
@@ -472,6 +534,11 @@ protected override void LoadComplete()
472534

473535
UserPlayingState.BindValueChanged(_ => updateOrientation());
474536

537+
// NOTE: no `true` (immediate-fire) flag — the initial apply is done inside
538+
// the refreshRateDelayMs scheduler above, so the cold-start texture-upload
539+
// burst completes under default GC latency. User-driven changes from the
540+
// settings dropdown (and the DeX auto-flip above, whose value is picked up
541+
// at defer-fire time) still take effect immediately via this subscription.
475542
performanceMode.BindValueChanged(e =>
476543
{
477544
try
@@ -482,7 +549,7 @@ protected override void LoadComplete()
482549
{
483550
Debug.WriteLine($"[osu!] Failed to toggle performance mode: {ex.Message}");
484551
}
485-
}, true);
552+
});
486553

487554
// Layer 3b — Oboe and Vulkan-probe initial-bind handling.
488555
//
@@ -492,10 +559,18 @@ protected override void LoadComplete()
492559
// local config), that synchronous fire would do native init on
493560
// the BDL load thread, in the silent cold-start window — exactly
494561
// when we are debugging a startup hang. Deferring the initial
495-
// fire via Scheduler (i.e. moving it onto the next Update tick on
496-
// the Update thread, after the game has finished loading) keeps
497-
// the cold-start path free of synchronous native init even when a
498-
// saved-true setting would otherwise force it.
562+
// fire via Scheduler.AddDelayed onto the same refreshRateDelayMs
563+
// timer that gates SustainedPerformanceMode / the initial refresh-
564+
// rate apply / the initial performance-mode apply keeps the cold-
565+
// start path free of synchronous native init even when a saved-
566+
// true setting would otherwise force it, AND ensures the native
567+
// init actually lands AFTER the cold-start Toolbar texture-upload
568+
// burst has drained.
569+
//
570+
// (Prior implementations used plain Schedule(...), which only
571+
// defers to the next Update tick — milliseconds, still well inside
572+
// the burst. The comment correctly described the intent — "after
573+
// the game has finished loading" — but the code under-delivered.)
499574
//
500575
// Default: defer (safe). Toggle off in settings to restore the
501576
// original immediate-init behaviour for A/B testing.
@@ -512,7 +587,7 @@ protected override void LoadComplete()
512587

513588
if (deferInit)
514589
{
515-
Schedule(() =>
590+
Scheduler.AddDelayed(() =>
516591
{
517592
try
518593
{
@@ -523,7 +598,7 @@ protected override void LoadComplete()
523598
{
524599
Debug.WriteLine($"[osu!] Deferred startup native init failed: {ex.Message}");
525600
}
526-
});
601+
}, refreshRateDelayMs);
527602
}
528603
else
529604
{
@@ -538,25 +613,11 @@ protected override void LoadComplete()
538613
}
539614
}
540615

541-
try
542-
{
543-
gameActivity.RunOnUiThread(() =>
544-
{
545-
try
546-
{
547-
int sources = (int)(InputSourceType.Touchscreen | InputSourceType.Stylus | InputSourceType.Mouse | InputSourceType.Touchpad);
548-
gameActivity.Window?.DecorView?.RequestUnbufferedDispatch(sources);
549-
}
550-
catch (Exception e)
551-
{
552-
Debug.WriteLine($"[osu!] Failed to request unbuffered touch dispatch: {e.Message}");
553-
}
554-
});
555-
}
556-
catch (Exception e)
557-
{
558-
Debug.WriteLine($"[osu!] Failed to schedule unbuffered dispatch: {e.Message}");
559-
}
616+
// NOTE: the trailing RequestUnbufferedDispatch(sources) that used to live
617+
// here has been moved into the refreshRateDelayMs Scheduler.AddDelayed block
618+
// above, so the DecorView attribute mutation lands after the cold-start
619+
// Toolbar texture-upload burst has drained. See the deferred block for the
620+
// full rationale.
560621
}
561622

562623
private void applyPerformanceOptimizations(bool enabled)

0 commit comments

Comments
 (0)