@@ -249,7 +249,33 @@ protected override void OnCreate(Bundle? savedInstanceState)
249249 {
250250 try
251251 {
252- GetSurface ( ) ? . Holder ? . AddCallback ( this ) ;
252+ var holder = GetSurface ( ) ? . Holder ;
253+
254+ if ( holder != null )
255+ {
256+ // Request RGBA8888 on the Android SurfaceHolder unconditionally BEFORE
257+ // registering our callback. Without this, Android defaults to RGB565
258+ // for the SurfaceView when no renderer explicitly requests a different
259+ // format — SDL3 only calls setFormat(RGBA8888) for OpenGL, not Vulkan,
260+ // so Vulkan sessions receive an RGB565 ANativeWindow. An RGB565 swapchain
261+ // is incompatible with our 8-bit-per-channel rendering pipeline and
262+ // causes a black screen followed by a native Draw-thread crash on Adreno
263+ // GPUs (evidenced by SDL_PIXELFORMAT_RGB565 + "drawable size 3088×1440"
264+ // in the runtime log for every Vulkan crash session). RGBA8888 is what
265+ // OpenGL already uses and is the correct baseline for all renderers.
266+ // Calling SetFormat before AddCallback ensures the format is stamped
267+ // on the SurfaceHolder before SDL creates the VkAndroidSurfaceKHR.
268+ try
269+ {
270+ holder . SetFormat ( global ::Android . Graphics . Format . Rgba8888 ) ;
271+ }
272+ catch ( Exception e )
273+ {
274+ Debug . WriteLine ( $ "[osu!] Failed to request RGBA8888 surface format: { e . Message } ") ;
275+ }
276+
277+ holder . AddCallback ( this ) ;
278+ }
253279 }
254280 catch ( Exception e )
255281 {
@@ -586,6 +612,43 @@ public void SurfaceCreated(ISurfaceHolder holder)
586612
587613 public void SurfaceChanged ( ISurfaceHolder holder , global ::Android . Graphics . Format format , int width , int height )
588614 {
615+ // Guard: if the Android surface materialised with a 16-bit pixel format (RGB565)
616+ // while Vulkan is configured, request a format change to RGBA8888 immediately.
617+ //
618+ // Root cause: SDL3 only calls setFormat(RGBA8888) for OpenGL, not Vulkan.
619+ // Android's default SurfaceView pixel format on many displays (especially high-
620+ // density landscape panels) is RGB565. An RGB565 ANativeWindow means the Vulkan
621+ // WSI can only negotiate R5G6B5_UNORM as the swapchain format, which is
622+ // incompatible with our 8-bit-per-channel pipeline and produces a black screen
623+ // followed by a native Draw-thread crash on Adreno GPUs.
624+ //
625+ // The proactive SetFormat(RGBA8888) call in the DecorView.Post lambda above is
626+ // the primary fix (runs before the Surface is typically created). This reactive
627+ // guard is the belt-and-braces fallback for timing windows where the Surface is
628+ // already created when the Post fires (e.g. rapid cold-starts, system-restored
629+ // windows). Calling SetFormat here triggers SurfaceDestroyed + SurfaceCreated +
630+ // SurfaceChanged with the corrected format; Veldrid's VkSurfaceKHR-loss recovery
631+ // picks up the new ANativeWindow and negotiates a proper BGRA/RGBA 8-bit swapchain.
632+ if ( format == global ::Android . Graphics . Format . Rgb565 && LogManagement . IsVulkanConfigured ( ) )
633+ {
634+ Logger . Log (
635+ "[osu!] Android surface pixel format RGB565 is incompatible with the Vulkan rendering pipeline " +
636+ "— requesting RGBA8888 and triggering a surface recreate. " +
637+ "This is the root cause of the Vulkan black-screen crash on Adreno (SDL_PIXELFORMAT_RGB565 in runtime log). " +
638+ "The next SurfaceChanged will carry the corrected format." ,
639+ LoggingTarget . Performance ,
640+ LogLevel . Important ) ;
641+
642+ try
643+ {
644+ holder . SetFormat ( global ::Android . Graphics . Format . Rgba8888 ) ;
645+ }
646+ catch ( Exception e )
647+ {
648+ Debug . WriteLine ( $ "[osu!] Failed to request RGBA8888 format change for Vulkan: { e . Message } ") ;
649+ }
650+ }
651+
589652 if ( width > 0 && height > 0 )
590653 {
591654 surfaceEvent . Set ( ) ;
0 commit comments