Skip to content

Commit f468cc5

Browse files
Fix Vulkan 9-screen tiling and flashing textures on Android (Samsung One UI / Adreno 740)
Root cause: Window.SetSustainedPerformanceMode(true) triggered a Samsung One UI display-mode transition that recreated the SurfaceView as RGB565. The reactive SurfaceChanged guard called SetFormat(RGBA8888) causing a second teardown. During that teardown, the ANativeWindow transiently reported scaled (dp) dimensions (1029×480 on a 3088×1440 3×-density panel). Veldrid baked those as the permanent swapchain size → 3×3 tiling, blurry textures, and ~40fps. Fixes: - Remove SetSustainedPerformanceMode(true) entirely; ADPF is covered by Oboe's setPerformanceHintEnabled and GC latency by AndroidHighPerformanceSessionManager - Add proactive Window.SetFormat(RGBA8888) before base.OnCreate() so SDL's SurfaceView is born with RGBA8888, eliminating the startup teardown - Upgrade reactive SurfaceChanged guard to also log to Runtime (not just Performance) so any future mid-session RGB565 resets are immediately visible - Update all stale comments that referenced SustainedPerformanceMode Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/3c1fa36e-ab75-466a-9437-b218eeb9fa8e Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent c7b9915 commit f468cc5

2 files changed

Lines changed: 56 additions & 39 deletions

File tree

osu.Android/OsuGameActivity.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,29 @@ protected override void OnCreate(Bundle? savedInstanceState)
225225
LogManagement.WipeShaderCacheOnceForVersion();
226226
CrashDiagnostics.WriteAliveMarker("LogManagement.WipeShaderCacheOnceForVersion (returned)");
227227

228+
// Stamp RGBA8888 at the Window level BEFORE SDL creates its SurfaceView inside
229+
// base.OnCreate(). Android's default SurfaceView pixel format on many high-density
230+
// Samsung / Qualcomm panels is RGB565. SDL3 only calls SurfaceHolder.setFormat(
231+
// RGBA8888) for the OpenGL path — the Vulkan path inherits the window default.
232+
// Setting the format here, before SDL attaches its SurfaceView, ensures the
233+
// SurfaceView is born with RGBA8888 and eliminates the format-change teardown
234+
// (SurfaceHolder.SetFormat in DecorView.Post) that otherwise fires mid-Vulkan-init
235+
// and can produce the "Draw thread did not acknowledge teardown within 250ms" warning.
236+
// The DecorView.Post call and the SurfaceChanged reactive guard are retained as
237+
// belt-and-braces fallbacks for timing windows or OEM variants where this hint is
238+
// not honoured by the SurfaceView allocation path.
239+
if (LogManagement.IsVulkanConfigured())
240+
{
241+
try
242+
{
243+
Window?.SetFormat(global::Android.Graphics.Format.Rgba8888);
244+
}
245+
catch (Exception e)
246+
{
247+
Debug.WriteLine($"[osu!] Pre-SDL Window.SetFormat(RGBA8888) failed (non-fatal): {e.Message}");
248+
}
249+
}
250+
228251
base.OnCreate(savedInstanceState);
229252

230253
// Wrap Platform.Init defensively: MAUI Essentials pulls in workload-version-sensitive
@@ -660,6 +683,13 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma
660683
// picks up the new ANativeWindow and negotiates a proper BGRA/RGBA 8-bit swapchain.
661684
if (format == global::Android.Graphics.Format.Rgb565 && LogManagement.IsVulkanConfigured())
662685
{
686+
Logger.Log(
687+
"[osu!] Android surface pixel format RGB565 detected mid-session (Vulkan path). " +
688+
"Requesting RGBA8888 and triggering a surface recreate. " +
689+
"If this fires after startup it indicates an OEM display-mode change " +
690+
"(e.g. SetSustainedPerformanceMode) reset the surface format.",
691+
LoggingTarget.Runtime,
692+
LogLevel.Important);
663693
Logger.Log(
664694
"[osu!] Android surface pixel format RGB565 is incompatible with the Vulkan rendering pipeline " +
665695
"— requesting RGBA8888 and triggering a surface recreate. " +

osu.Android/OsuGameAndroid.cs

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ protected override void LoadComplete()
379379
//
380380
// Pinning Update + Draw + Input to a 5-core subset (mask 0xF8 on SD8G2) is the
381381
// ONLY unconditional Android-specific synchronous mutation we still perform
382-
// during the cold-start window — every other customisation (SustainedPerformanceMode,
382+
// during the cold-start window — every other customisation (
383383
// RequestUnbufferedDispatch, refresh-rate selection, Oboe / Vulkan-probe init,
384384
// performance-mode GC-latency flip) is already deferred behind the
385385
// refreshRateDelayMs scheduler below. Field logs.zip on v2026.423.176 show both
@@ -569,21 +569,28 @@ protected override void LoadComplete()
569569
Debug.WriteLine($"[osu!] TameBackgroundThreads (initial) failed: {e.Message}");
570570
}
571571

572-
// Sustained performance mode is applied LATER, together with the deferred
573-
// display-mode / GC-latency work below. See the Scheduler.AddDelayed block
574-
// further down (after base.LoadComplete()) that schedules the first apply
575-
// on a refreshRateDelayMs timer. Running
576-
// Window.SetSustainedPerformanceMode(true) synchronously here — during the
577-
// Toolbar cold-start texture-upload burst and the Vulkan swapchain bring-up —
578-
// has been observed to race the Draw thread on Samsung One UI / Adreno panels:
579-
// the window-flag mutation round-trips through ViewRootImpl.setPrivateFlags
580-
// and can partially reconfigure the Surface while vkAcquireNextImageKHR is in
581-
// flight, stalling the present queue. Update keeps ticking (so neither the
582-
// managed nor the native watchdog ever dumps), the screen never updates, and
583-
// ~10 s later Android raises a MotionEvent input-dispatch ANR — the exact
584-
// cold-start "black screen → no touch → ANR" fingerprint reported across
585-
// multiple v174 launches in logs.zip. Deferring to the same window used by
586-
// SelectHighestRefreshRate moves the mutation behind the texture-upload burst.
572+
// Window.SetSustainedPerformanceMode is intentionally NOT called anywhere.
573+
//
574+
// On Samsung One UI / Adreno devices, calling SetSustainedPerformanceMode(true)
575+
// triggers a non-seamless display-mode transition (even when deferred behind the
576+
// texture-upload burst). The transition momentarily destroys the SurfaceView,
577+
// which resets the surface pixel format back to the Android default (RGB565 on
578+
// high-density Samsung panels). Our SurfaceChanged reactive guard then calls
579+
// SurfaceHolder.SetFormat(RGBA8888), causing a second surface-destroy/recreate
580+
// cycle. During this second cycle the ANativeWindow transiently reports the
581+
// display's scaled (dp) dimensions — 1029×480 on a 3088×1440 3×-density panel —
582+
// instead of the physical pixel dimensions. Veldrid reads those dimensions from
583+
// vkGetPhysicalDeviceSurfaceCapabilitiesKHR during its VkSurfaceKHR-loss
584+
// recovery, creates a permanent swapchain at 1029×480, and SurfaceFlinger tiles
585+
// that sub-screen image 3×3 to fill the display. The result is the "9 screens"
586+
// artifact, blurry/flashing textures, and a sustained FPS drop observed on
587+
// Galaxy S24 Ultra (Adreno 740, One UI 7, Android 15) with Vulkan enabled.
588+
//
589+
// Removing the call eliminates the mid-session surface teardown. ADPF performance
590+
// hinting is already provided by Oboe's setPerformanceHintEnabled(true) (set
591+
// during stream open in oboe_bridge.cpp), and GC low-latency is handled by
592+
// AndroidHighPerformanceSessionManager (SustainedLowLatency GCSettings) which
593+
// covers the same thermal/responsiveness goals without touching the Surface.
587594

588595
base.LoadComplete();
589596

@@ -688,27 +695,6 @@ protected override void LoadComplete()
688695
Debug.WriteLine($"[osu!] Deferred SelectHighestRefreshRate failed: {ex.Message}");
689696
}
690697

691-
// Deferred sustained-performance-mode apply. See the comment block
692-
// before base.LoadComplete() above for the rationale (Samsung One UI /
693-
// Adreno Surface reconfigure race with vkAcquireNextImageKHR during the
694-
// cold-start texture-upload burst). By the time this fires the
695-
// swapchain has long since stabilised.
696-
try
697-
{
698-
gameActivity.RunOnUiThread(() =>
699-
{
700-
try { gameActivity.Window?.SetSustainedPerformanceMode(true); }
701-
catch (Exception e)
702-
{
703-
Debug.WriteLine($"[osu!] Failed to enable sustained performance mode: {e.Message}");
704-
}
705-
});
706-
}
707-
catch (Exception e)
708-
{
709-
Debug.WriteLine($"[osu!] Failed to dispatch sustained performance mode toggle to UI thread: {e.Message}");
710-
}
711-
712698
// Deferred initial application of the user's performance-mode setting.
713699
// The BindValueChanged registration below is WITHOUT the immediate-fire
714700
// flag, so the very first apply (which may flip GCSettings.LatencyMode
@@ -1033,7 +1019,7 @@ protected override void LoadComplete()
10331019
// the BDL load thread, in the silent cold-start window — exactly
10341020
// when we are debugging a startup hang. Deferring the initial
10351021
// fire via Scheduler.AddDelayed onto the same refreshRateDelayMs
1036-
// timer that gates SustainedPerformanceMode / the initial refresh-
1022+
// timer that gates the initial refresh-
10371023
// rate apply / the initial performance-mode apply keeps the cold-
10381024
// start path free of synchronous native init even when a saved-
10391025
// true setting would otherwise force it, AND ensures the native
@@ -1149,8 +1135,9 @@ private void applyPerformanceOptimizations(bool enabled)
11491135
{
11501136
try
11511137
{
1152-
// Sustained performance mode is always on (set in LoadComplete).
11531138
// The performance toggle controls the high-perf GC session only.
1139+
// (Window.SetSustainedPerformanceMode is intentionally not called —
1140+
// see the comment before base.LoadComplete() for the full rationale.)
11541141
if (enabled)
11551142
{
11561143
highPerformanceSession ??= highPerformanceSessionManager.BeginSession();

0 commit comments

Comments
 (0)