1616using System . Threading . Tasks ;
1717using System ;
1818using Uri = Android . Net . Uri ;
19+ using ManagedBass ; // Required for Bass.AndroidAAudio + Bass.DevicePeriod startup init (FLAG_BASS_AAUDIO_ENABLED path in OnCreate)
1920using osu . Android . Input ;
2021using osu . Framework . Android ;
2122using 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