Skip to content

Android: fix OpenGL black screen regression from PR #286 - #287

Merged
winnerspiros merged 3 commits into
masterfrom
copilot/fix-opengl-black-screen-issue
May 1, 2026
Merged

Android: fix OpenGL black screen regression from PR #286#287
winnerspiros merged 3 commits into
masterfrom
copilot/fix-opengl-black-screen-issue

Conversation

Copilot AI commented May 1, 2026

Copy link
Copy Markdown
  • Identify latest published versions: ppy.osu.Framework 2026.501.2 (osu-framework pack run Optimize HitCircleOverlapMarker colour updates #18, commit e72591b4, includes veldrid 4.9.18); ppy.Veldrid.SPIRV unchanged at 1.0.15-gb268bf39ea
  • Update osu.Game/osu.Game.csproj: ppy.osu.Framework 2026.501.12026.501.2
  • Update osu.Android.props: ppy.osu.Framework.Android 2026.501.12026.501.2
  • Update osu.iOS.props: ppy.osu.Framework.iOS 2026.501.12026.501.2
Original prompt

Problem

After PR #286 ("Android: fix Vulkan black screen by forcing RGBA8888 on the SurfaceHolder") was merged, users are now reporting an intermittent OpenGL black screen on Android: the game opens, audio plays, clicks register on UI elements, but the visible surface is fully black. The new diagnostic warning

Draw thread did not acknowledge surface teardown within 250ms; proceeding anyway to avoid ANR.

(emitted by osu.Framework.Android/AndroidGameSurface.cs::SurfaceDestroyed, see https://github.com/winnerspiros/osu-framework/blob/27ed3e29d8b8f84c0cd7d4f2a64e28b399446457/osu.Framework.Android/AndroidGameSurface.cs#L122) is shown in the top-right when this happens.

The Vulkan renderer is also still black-screening on Adreno 740 even after PR #286 — but that part is being fixed in a companion PR against winnerspiros/veldrid (swapchain preTransform plumbing). This PR is scoped to the OpenGL regression only.

Root cause of the OpenGL regression

PR #286 added the following block to osu.Android/OsuGameActivity.cs (see

}
catch (Exception e)
{
Debug.WriteLine($"[osu!] MAUI Platform.Init failed (non-fatal): {e.Message}");
}
updateDeXStatus(null);
// Posting the surface-callback registration onto the UI thread loop is intentional
// (the SurfaceView may not be attached yet at OnCreate time). Guard the body of the
// lambda — a later race with activity teardown can make AddCallback throw.
Window?.DecorView.Post(() =>
{
try
{
var holder = GetSurface()?.Holder;
if (holder != null)
{
// Request RGBA8888 on the Android SurfaceHolder unconditionally BEFORE
// registering our callback. Without this, Android defaults to RGB565
// for the SurfaceView when no renderer explicitly requests a different
// format — SDL3 only calls setFormat(RGBA8888) for OpenGL, not Vulkan,
// so Vulkan sessions receive an RGB565 ANativeWindow. An RGB565 swapchain
// is incompatible with our 8-bit-per-channel rendering pipeline and
// causes a black screen followed by a native Draw-thread crash on Adreno
// GPUs (evidenced by SDL_PIXELFORMAT_RGB565 + "drawable size 3088×1440"
// in the runtime log for every Vulkan crash session). RGBA8888 is what
// OpenGL already uses and is the correct baseline for all renderers.
// Calling SetFormat before AddCallback ensures the format is stamped
// on the SurfaceHolder before SDL creates the VkAndroidSurfaceKHR.
try
{
holder.SetFormat(global::Android.Graphics.Format.Rgba8888);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to request RGBA8888 surface format: {e.Message}");
}
holder.AddCallback(this);
}
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to register SurfaceHolder callback: {e.Message}");
}
});
handleIntent(Intent);
if (Window != null)
{
):

Window?.DecorView.Post(() =>
{
    var holder = GetSurface()?.Holder;
    if (holder != null)
    {
        // Unconditionally request RGBA8888 on the Android SurfaceHolder ...
        try { holder.SetFormat(global::Android.Graphics.Format.Rgba8888); }
        catch (Exception e) { Debug.WriteLine($"... {e.Message}"); }
        holder.AddCallback(this);
    }
});

The intent was correct for the Vulkan path (SDL3 only calls setFormat(RGBA8888) from its own GL-context init code, never from the Vulkan path, so Vulkan was inheriting the Android default of RGB565). But the call is currently fired unconditionally for every renderer, including OpenGL.

On the OpenGL path, SDL3's own SDLSurface callback already calls setFormat(RGBA8888) from inside SDL during EGL surface creation. Our posted lambda then calls setFormat(RGBA8888) a second time, after SDL has already bound its EGL surface to the underlying ANativeWindow. Per the Android SurfaceHolder.setFormat contract, any change (even to the same value, on some OEMs) triggers a surface re-create cycle: surfaceDestroyedsurfaceCreatedsurfaceChanged. That cycle:

  1. Trips the new 250ms drawThreadAcknowledgedTeardown wait in AndroidGameSurface.SurfaceDestroyed (because the Draw thread is mid-base.DrawFrame() and can't reach NotifyDrawThreadIdle() in time) — producing the user-visible warning.
  2. Leaves SDL's EGL surface bound to a now-dead ANativeWindow. All subsequent eglSwapBuffers calls silently no-op against a destroyed surface. The Update thread, audio, input handling and Logic all continue to function — exactly matching the user's report ("I can click elements, music plays, but it's black").
  3. Is racy — whether the post() lambda runs before or after SDL's own setFormat decides whether the user sees a working frame or the black screen. This explains the intermittent reproduction.

Fix

Gate the holder.SetFormat(Rgba8888) call so it only runs when the configured renderer is Vulkan. SDL3 already handles the format correctly for OpenGL/GLES, so we must not re-stamp it.

We already have a helper that reads framework.ini to determine the configured renderer: LogManagement.IsVulkanConfigured() (used elsewhere in OsuGameAndroid.handleVulkanProbeChanged, see

try { vulkanConfigured = LogManagement.IsVulkanConfigured(); }
). Reuse it.

Required code change in osu.Android/OsuGameActivity.cs

Replace the unconditional SetFormat block with a Vulkan-gated version. Suggested implementation:

Window?.DecorView.Post(() =>
{
    try
    {
        var holder = GetSurface()?.Holder;
        if (holder == null) return;

        // Only request RGBA8888 on the SurfaceHolder when the configured
        // renderer is Vulkan. SDL3 already calls setFormat(RGBA8888) for
        // OpenGL/GLES from its own EGL surface initialization, and calling
        // setFormat() a second time AFTER SDL has bound its EGL surface
        // forces Android to recreate the surface (surfaceDestroyed →
        // surfaceCreated → surfaceChanged) mid-frame. That:
        //   1. Trips the 250ms drawThreadAcknowledgedTeardown wait in
        //      AndroidGameSurface.SurfaceDestroyed (visible warning in HUD).
        //   2. Leaves SDL's EGL surface bound to a destroyed ANativeWindow,
        //      causing eglSwapBuffers to silently no-op → permanent black
        //      screen while Update/Audio/Input keep running.
        // SDL3 does NOT call setFormat on the Vulkan path, so Vulkan still
        // needs this stamping to avoid the RGB565 default that crashes Adreno.
        bool isVulkan = false;
        try { isVulkan = LogManagement.IsVulkanConfigured(); }
        catch (Exc...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI changed the title [WIP] Fix intermittent OpenGL black screen issue on Android Android: fix OpenGL black screen regression from PR #286 May 1, 2026
Copilot AI requested a review from winnerspiros May 1, 2026 14:40
Copilot AI requested a review from winnerspiros May 1, 2026 16:15
@winnerspiros
winnerspiros marked this pull request as ready for review May 1, 2026 16:31
Copilot AI review requested due to automatic review settings May 1, 2026 16:31
@winnerspiros
winnerspiros merged commit c836ef3 into master May 1, 2026
13 of 15 checks passed
@gitar-bot

gitar-bot Bot commented May 1, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts Android surface format handling to prevent an OpenGL black-screen regression introduced by the earlier Vulkan RGB565 workaround, while also bumping framework package versions to the latest published build.

Changes:

  • Bump ppy.osu.Framework / Android / iOS package references from 2026.501.12026.501.2.
  • Gate SurfaceHolder.SetFormat(Rgba8888) in OsuGameActivity so it only runs for Vulkan-configured sessions (instead of unconditionally).
  • Add runtime logging indicating whether the surface format stamp was applied or skipped.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
osu.Android/OsuGameActivity.cs Gates SurfaceHolder.SetFormat(Rgba8888) behind a Vulkan check to avoid triggering OpenGL surface recreate cycles.
osu.Game/osu.Game.csproj Bumps ppy.osu.Framework package version.
osu.Android.props Bumps ppy.osu.Framework.Android package version.
osu.iOS.props Bumps ppy.osu.Framework.iOS package version.
Comments suppressed due to low confidence (1)

osu.Game/osu.Game.csproj:45

  • The comment explaining the ppy.Veldrid.SPIRV pin still references ppy.osu.Framework 2026.501.1, but this PR bumps the framework reference to 2026.501.2. Please update the comment so it matches the actual pinned framework version (and remains accurate for future maintenance).
    <!--
      Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
      `ppy.osu.Framework 2026.501.1` was compiled against. This version is the only
      one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +269 to +273
bool isVulkan = false;
try { isVulkan = LogManagement.IsVulkanConfigured(); }
catch (Exception e) { Debug.WriteLine($"[osu!] SurfaceHolder format gate: IsVulkanConfigured failed, defaulting to skip SetFormat: {e.Message}"); }

if (isVulkan)

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The surface-format gate only checks LogManagement.IsVulkanConfigured(), which returns true only when framework.ini contains Renderer=Vulkan. If a user has Renderer=Automatic and the framework resolves that to Vulkan (which LogManagement.NormaliseFrameworkIniRendererDefault()’s XML docs state is the Android default), this will skip SetFormat() and the Vulkan path can still inherit RGB565. Consider treating Automatic as Vulkan here (or introduce a helper like LogManagement.ShouldForceRgba8888Surface() that covers both Vulkan and Automatic when Vulkan is actually in use), so Vulkan-by-automatic doesn’t regress.

Copilot uses AI. Check for mistakes.
Comment on lines +277 to 288
holder.SetFormat(global::Android.Graphics.Format.Rgba8888);
Logger.Log("[osu!] SurfaceHolder.SetFormat(Rgba8888) applied (Vulkan renderer).", LoggingTarget.Runtime, LogLevel.Important);
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Failed to request RGBA8888 surface format: {e.Message}");
}
}
catch (Exception e)
else
{
Debug.WriteLine($"[osu!] Failed to request RGBA8888 surface format: {e.Message}");
Logger.Log("[osu!] SurfaceHolder.SetFormat skipped (OpenGL/Auto renderer — SDL3 handles format).", LoggingTarget.Runtime, LogLevel.Important);
}

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Logger.Log(..., LogLevel.Important) messages will be emitted on every startup (including the non-Vulkan path). Given that Important-level logs are user-visible in the in-game log overlay, this is likely to create persistent on-screen noise. Suggest lowering these to LogLevel.Verbose/Debug (or removing the non-Vulkan log entirely), keeping Important reserved for actionable warnings like the RGB565+Vulkan guard below.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants