Skip to content

Commit 20856d2

Browse files
authored
Merge pull request #304 from winnerspiros/copilot/fix-vulkan-crash-issue
Android audio: replace dual Oboe/AAudio checkboxes with unified backend dropdown; show active backend in FPS additional info
2 parents b780686 + d608aa7 commit 20856d2

14 files changed

Lines changed: 315 additions & 59 deletions

File tree

osu.Android.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
</PropertyGroup>
100100

101101
<ItemGroup>
102-
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.505.1" />
102+
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.506.2" />
103103
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
104104
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
105105
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.

osu.Android/AndroidStartupFlags.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ internal static class AndroidStartupFlags
5353
/// </summary>
5454
public const string FLAG_VERBOSE_LOGGING_ENABLED = "android_startup_enable_verbose_logging.flag";
5555

56+
/// <summary>
57+
/// BASS AAudio opt-in sentinel. Presence ⇒ "user has enabled BASS AAudio output".
58+
/// Absence ⇒ "default — BASS uses AudioTrack".
59+
/// When set, <see cref="OsuGameActivity.OnCreate"/> calls <c>Bass.AndroidAAudio = true</c>
60+
/// and <c>Bass.DevicePeriod = -512</c> before any BASS initialisation, so BASS opens
61+
/// an AAudio device instead of AudioTrack. On Android ≥ 8.0 this gives lower intrinsic
62+
/// BASS output latency; on older devices BASS falls back to AudioTrack automatically.
63+
/// Orthogonal to the Oboe bridge (<see cref="osu.Game.Configuration.OsuSetting.AndroidLowLatencyAudio"/>):
64+
/// when Oboe is also enabled it overrides BASS's output backend entirely via the
65+
/// GlobalMixerHandle decode-only path, so this flag only has a perceptible effect
66+
/// when Oboe is disabled.
67+
/// </summary>
68+
public const string FLAG_BASS_AAUDIO_ENABLED = "android_startup_enable_bass_aaudio.flag";
69+
5670
/// <summary>
5771
/// "Startup in progress" sentinel. Dropped near the very top of <see cref="OsuGameActivity.OnCreate"/>
5872
/// and cleared a few seconds after <c>OsuGame.LoadComplete</c> by <see cref="OsuGameAndroid"/>.

osu.Android/AndroidStartupSafeMode.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ internal static class AndroidStartupSafeMode
6161
public static bool IsActive => isActive;
6262

6363
/// <summary>
64-
/// True iff <see cref="IsActive"/> was set by the Draw-thread native-crash trigger
64+
/// True iff <see cref="IsActive"/> was set by the native-crash trigger
6565
/// (rather than by the <see cref="AndroidStartupFlags.FLAG_STARTUP_IN_PROGRESS"/>
6666
/// "previous launch died before LoadComplete" sentinel). Lets log lines explain
6767
/// WHICH safety net forced the conservative defaults.
68+
/// The crash may have been on the Draw thread or the SDL/Vulkan-init thread.
6869
/// </summary>
6970
public static bool DrawThreadNativeCrashTriggered => drawThreadNativeCrashTriggered;
7071

@@ -151,9 +152,9 @@ private static void applyIfPreviousDrawThreadNativeCrash()
151152
{
152153
CrashDiagnostics.AppendDiagnosticBlock(
153154
"\n=========================================================\n"
154-
+ "=== ANDROID STARTUP SAFE-MODE ACTIVATED (Draw-thread crash trigger) ===\n"
155+
+ "=== ANDROID STARTUP SAFE-MODE ACTIVATED (native crash trigger) ===\n"
155156
+ $" utc_time = {DateTime.UtcNow:O}\n"
156-
+ " reason = previous launch crashed natively on the Draw thread\n"
157+
+ " reason = previous launch crashed natively on the Draw/SDL thread\n"
157158
+ $" signal = {crash.Value.Signal}\n"
158159
+ $" thread_name = {crash.Value.ThreadName}\n"
159160
+ $" top_frame = {crash.Value.TopFrame}\n"

osu.Android/CrashDiagnostics.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,11 +624,17 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
624624
|| signal.StartsWith("SIGABRT", StringComparison.Ordinal);
625625
if (!isFatalSignal) return null;
626626

627-
// Match the framework's draw thread name. The native handler
627+
// Match threads that host Veldrid/Vulkan work. The native handler
628628
// writes a TRUNCATED thread name (Linux pthread_setname is
629629
// capped at 16 chars including NUL) so "Draw (GameThread)"
630630
// appears as "Draw (GameThrea" — substring match is correct.
631-
if (!threadName.StartsWith("Draw", StringComparison.Ordinal)) return null;
631+
// SDL names its main render/init thread "SDLThread" (9 chars, well
632+
// under the 16-char limit — never truncated); a null-function-pointer
633+
// Vulkan crash (SIGSEGV pc=0x0) on this thread during startup should
634+
// also activate safe-mode.
635+
bool isKnownCrashThread = threadName.StartsWith("Draw", StringComparison.Ordinal)
636+
|| threadName.StartsWith("SDL", StringComparison.Ordinal);
637+
if (!isKnownCrashThread) return null;
632638

633639
string topFrame = extractTopFrame(block);
634640

@@ -677,7 +683,12 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
677683
|| signal.StartsWith("SIGABRT", StringComparison.Ordinal);
678684
if (!isFatalSignal) return null;
679685

680-
if (!thread.StartsWith("Draw", StringComparison.Ordinal)) return null;
686+
// Match the same thread set as the full-header path above.
687+
// "SDLThread" (9 chars) is below the 16-char truncation limit and
688+
// matches the "SDL" prefix check without ambiguity.
689+
bool isKnownCrashThread = thread.StartsWith("Draw", StringComparison.Ordinal)
690+
|| thread.StartsWith("SDL", StringComparison.Ordinal);
691+
if (!isKnownCrashThread) return null;
681692

682693
string fingerprint = (uptime != null && pid != null)
683694
? $"u{uptime}-p{pid}"

osu.Android/OsuGameActivity.cs

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Threading.Tasks;
1717
using System;
1818
using Uri = Android.Net.Uri;
19+
using ManagedBass; // Required for Bass.AndroidAAudio + Bass.DevicePeriod startup init (FLAG_BASS_AAUDIO_ENABLED path in OnCreate)
1920
using osu.Android.Input;
2021
using osu.Framework.Android;
2122
using osu.Game.Database;
@@ -264,6 +265,31 @@ protected override void OnCreate(Bundle? savedInstanceState)
264265
}
265266
}
266267

268+
// BASS AAudio: if the user opted in, tell BASS to open an AAudio device instead
269+
// of AudioTrack before the host creates its AudioThread and calls Bass.Init().
270+
// Bass.AndroidAAudio must be set before Bass.Init() — reading the sentinel here
271+
// (before base.OnCreate, which starts the SDL+game machinery) is the earliest
272+
// safe point. On Android < 8.0 BASS falls back to AudioTrack automatically.
273+
// When the Oboe bridge (AndroidLowLatencyAudio) is also active it overrides
274+
// BASS's own output via the GlobalMixerHandle decode path anyway, so this flag
275+
// only materially changes behaviour when Oboe is disabled.
276+
if (AndroidStartupFlags.IsSet(AndroidStartupFlags.FLAG_BASS_AAUDIO_ENABLED))
277+
{
278+
try
279+
{
280+
Bass.AndroidAAudio = true;
281+
// -512 requests a 512-sample AAudio buffer (≈ 11.6 ms at 44 100 Hz),
282+
// giving a good latency/stability trade-off. The negative sign means
283+
// "specify in samples rather than milliseconds" (BASS 4Android convention).
284+
Bass.DevicePeriod = -512;
285+
CrashDiagnostics.WriteAliveMarker("Bass.AndroidAAudio = true (DevicePeriod = -512)");
286+
}
287+
catch (Exception e)
288+
{
289+
Debug.WriteLine($"[osu!] Bass.AndroidAAudio init failed (non-fatal): {e.Message}");
290+
}
291+
}
292+
267293
base.OnCreate(savedInstanceState);
268294

269295
// Wrap Platform.Init defensively: MAUI Essentials pulls in workload-version-sensitive
@@ -763,11 +789,24 @@ public void SurfaceCreated(ISurfaceHolder holder)
763789
if (handle == IntPtr.Zero)
764790
return;
765791

766-
// Reset per-lifecycle flags. The new surface has not yet reported its format
767-
// (SurfaceChanged fires after SurfaceCreated), and any previous pending-format
768-
// stamp no longer applies to this new surface instance.
792+
// Reset the format-tracking field: the new surface has not yet reported its
793+
// format (SurfaceChanged fires after SurfaceCreated).
794+
//
795+
// Intentionally do NOT reset setFormatPending here. If we previously called
796+
// SetFormat(Rgba8888) to fix an RGB565 surface, setFormatPending stays true
797+
// across the resulting SurfaceDestroyed → SurfaceCreated cycle so that if the
798+
// new surface ALSO arrives as RGB565 (i.e. the SetFormat had no effect on this
799+
// device) we do not fire the reactive guard a second time — that would chain
800+
// another teardown and produce a duplicate "[osu!] Android surface pixel format
801+
// RGB565 detected (Vulkan path)" log message in the overlay.
802+
//
803+
// The flag lifecycle is:
804+
// false → set to true when RGB565 guard fires and SetFormat is called
805+
// true → released back to false in SurfaceChanged when RGBA8888 is confirmed
806+
// (the SetFormat worked; future RGB565 events can fire the guard again)
807+
// true → stays true if the next SurfaceChanged also reports RGB565
808+
// (SetFormat had no effect; guard is suppressed to avoid chaining)
769809
lastSurfaceFormat = 0;
770-
setFormatPending = false;
771810

772811
IntPtr newRef = global::Android.Runtime.JNIEnv.NewGlobalRef(handle);
773812

@@ -822,7 +861,24 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma
822861
// the surface that arrives after the first teardown also briefly reports RGB565
823862
// (e.g. during a compositor mode transition), which would chain teardowns and
824863
// prevent the draw thread from ever acknowledging either one within 250 ms.
825-
if (format == global::Android.Graphics.Format.Rgb565 && LogManagement.IsVulkanConfigured() && !setFormatPending)
864+
// Unlike the old design (where setFormatPending was reset in SurfaceCreated),
865+
// the flag now persists across the SurfaceDestroyed→SurfaceCreated cycle and is
866+
// only released here when the surface is confirmed as RGBA8888. That prevents
867+
// the duplicate "[osu!] Android surface pixel format RGB565 detected" log message
868+
// that appeared when SetFormat did not change the format on certain Samsung/Adreno
869+
// devices (surface born as RGB565 again after the teardown).
870+
//
871+
// !AndroidStartupSafeMode.IsActive: safe-mode sessions always run OpenGL (via
872+
// ForceOpenGLRendererIfSafeMode). After LoadComplete, RestoreRendererAfterSafeMode
873+
// writes "Vulkan" back to framework.ini so IsVulkanConfigured() returns true for
874+
// the rest of that session — but the runtime renderer is still OpenGL. Firing the
875+
// RGB565 guard in that window would call SetFormat unnecessarily (RGB565 is fine
876+
// for OpenGL) and produce a mid-session surface teardown with a confusing
877+
// "(Vulkan path)" log message in the overlay.
878+
if (format == global::Android.Graphics.Format.Rgb565
879+
&& LogManagement.IsVulkanConfigured()
880+
&& !setFormatPending
881+
&& !AndroidStartupSafeMode.IsActive)
826882
{
827883
setFormatPending = true;
828884

@@ -860,6 +916,16 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma
860916
return;
861917
}
862918

919+
// Release the pending-format guard once the surface is confirmed RGBA8888.
920+
// This allows future RGB565 detection (e.g. after a display-mode change that
921+
// would legitimately reset the format) while still blocking a second spurious
922+
// fire during the immediate teardown+recreate that follows our own SetFormat call.
923+
if (format == global::Android.Graphics.Format.Rgba8888 && setFormatPending)
924+
{
925+
setFormatPending = false;
926+
Debug.WriteLine("[osu!] Surface format confirmed RGBA8888 — pending-format guard released.");
927+
}
928+
863929
if (width > 0 && height > 0)
864930
{
865931
surfaceEvent.Set();
@@ -898,7 +964,7 @@ public void SurfaceDestroyed(ISurfaceHolder holder)
898964
}
899965
}
900966

901-
public override void OnConfigurationChanged(Configuration newConfig)
967+
public override void OnConfigurationChanged(global::Android.Content.Res.Configuration newConfig)
902968
{
903969
base.OnConfigurationChanged(newConfig);
904970
bool wasDeX = IsDeX;
@@ -921,7 +987,7 @@ public override void OnConfigurationChanged(Configuration newConfig)
921987
}
922988
}
923989

924-
private void updateDeXStatus(Configuration? config)
990+
private void updateDeXStatus(global::Android.Content.Res.Configuration? config)
925991
{
926992
bool wasDeX = IsDeX;
927993
IsDeX = (config ?? Resources?.Configuration)?.UiMode.HasFlag(UiMode.TypeDesk) ?? false;

0 commit comments

Comments
 (0)