Skip to content

Bump framework to 2026.422.1; root Android Surface managed peer to fix SDLActivity SIGSEGV#242

Merged
winnerspiros merged 5 commits intomasterfrom
copilot/update-osu-framework-fixes
Apr 22, 2026
Merged

Bump framework to 2026.422.1; root Android Surface managed peer to fix SDLActivity SIGSEGV#242
winnerspiros merged 5 commits intomasterfrom
copilot/update-osu-framework-fixes

Conversation

Copy link
Copy Markdown

Copilot AI commented Apr 22, 2026

native_crash.log shows the APK looping through two phases: (1) Veldrid.VeldridException: The Swapchain's underlying surface has been lost aborts the Draw thread, then (2) on relaunch SDLActivity dies with SIGSEGV inside libart.so JNI handling. The root cause of (2) is that OsuGameActivity.SurfaceCreated only kept a JNI global ref on surface.Handle — the local Surface wrapper became GC-eligible the moment the method returned, and .NET-for-Android's finaliser then released the underlying Java peer that the global ref still pointed into.

Changes

  • Bump ppy.osu.Framework{,.Android,.iOS} 2026.420.22026.422.1 (osu.Game.csproj, osu.Android.props, osu.iOS.props). Pulls in winnerspiros/osu-framework PR Freeze CatcherTrail animation at generation time #17:

    • Re-snapshot IAndroidGraphicsSurface.SurfaceHandle immediately before vkCreateAndroidSurfaceKHR (managed exception instead of driver SIGSEGV when the surface is destroyed concurrently).
    • Recover from transient surface-lost on SwapBuffers / Resize (skip frame, force swapchain rebuild on next BeginFrame, bail out only after 60 consecutive failures).
    • Veldrid submodule bump to f85dd26.
  • Root the Android Surface managed peer (osu.Android/OsuGameActivity.cs):

    • New heldSurface field + surfaceLock serialise SurfaceCreated / SurfaceDestroyed so SDL/Veldrid can never see a half-torn-down state.
    • Creation publishes the new managed root before swapping in the new global ref, then disposes the previous wrapper after the old ref is gone.
    • Destruction inverts the order: free global ref → drop managed root.
    • GetSurfaceGlobalRef() now uses Volatile.Read for ordered unlocked reads on the SDL hot path.
  • Catch up ppy/master through a4f79f7 (PRs Refactor audio preview logic in ranked play cards to match expectations while hopefully not looking buggy anymore ppy/osu#37463 + Apply different workaround to fix ranked play single thread audio issues ppy/osu#37477, ranked play single-thread audio workaround). Conflict in RankedPlayCard.SongPreview.cs resolved in favour of our existing late-bind approach (Enabled/CardHovered subscribed inside the LoadComponentAsync callback, so they cannot fire pre-load — equivalent in effect to upstream's deferred-Schedule). New TestPreviewStopsOnEnteringGameplay brought in.

  • Docs/CI: refresh stale 2026.421.1 reference in local-packages/README.md. CI workflows already authenticate to the winnerspiros GitHub Packages feed in every restore job; no version pinning lives outside the .csproj/.props, so nothing else to touch.

Crash-relevant snippet

public void SurfaceDestroyed(ISurfaceHolder holder)
{
    lock (surfaceLock)
    {
        surfaceEvent.Reset();

        // Release the global ref BEFORE dropping the managed root — the inverse
        // of SurfaceCreated. Disposing heldSurface first would let the Java peer
        // be released while a stale global ref to it is still in flight.
        IntPtr oldRef = Interlocked.Exchange(ref surfaceGlobalRef, IntPtr.Zero);
        if (oldRef != IntPtr.Zero)
            JNIEnv.DeleteGlobalRef(oldRef);

        heldSurface?.Dispose();
        heldSurface = null;
    }
}

Bartłomiej Dach and others added 5 commits April 22, 2026 12:48
…ns while hopefully not looking buggy anymore (ppy#37463)

RFC.

See ppy#37453 (comment) for
why.

Of note:

- To facilitate mutual exclusivity of playback `PlayerHandOfCards`
maintains a bindable pointing at the currently playing song preview.
- Because of how card drawables are passed between multiple parenting
drawables, some of which are and some of which are not
`PlayerHandOfCards` instances, DI fails horribly at working with this
bindable unless it is manually managed. See relevant overrides in
`PlayerHandOfCards`.
- I renamed one of the overloads of `HandOfCards.RemoveCard()` to
`DetachCard()` because I found the fact that there are two overloads of
one method that do WILDLY DIFFERENT THINGS utterly *asinine*. (One
overload scrapes the `RankedPlayCard` out for you to plop elsewhere. One
*drops it on the floor entirely*.)

This took way too long to write.
…ues (ppy#37477)

Thing to make release happen.

Reverts ppy#37453
Reverts ppy#37463
Alternative to ppy#37473

Not that I disagree with any of these but I'm just looking to return to
what works so we can do a release because we're on a clock here for
other reasons.

Test which should work but doesn't, so I'm not adding:

```diff
diff --git a/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs b/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
index f747004..eb8e360d1e 100644
--- a/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
+++ b/osu.Game.Tests/Visual/RankedPlay/TestSceneOpponentPickScreen.cs
@@ -1,12 +1,17 @@
 // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
 // See the LICENCE file in the repository root for full licence text.
 
+using System.Linq;
+using NUnit.Framework;
 using osu.Framework.Extensions;
+using osu.Framework.Testing;
 using osu.Game.Online.API;
 using osu.Game.Online.Multiplayer;
 using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
 using osu.Game.Online.Rooms;
 using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
+using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
+using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
 
 namespace osu.Game.Tests.Visual.RankedPlay
 {
@@ -14,6 +19,8 @@ public partial class TestSceneOpponentPickScreen : RankedPlayTestScene
     {
         private RankedPlayScreen screen = null!;
 
+        private readonly BeatmapRequestHandler requestHandler = new BeatmapRequestHandler();
+
         public override void SetUpSteps()
         {
             base.SetUpSteps();
@@ -26,8 +33,6 @@ public override void SetUpSteps()
             AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
             AddUntilStep("screen loaded", () => screen.IsLoaded);
 
-            var requestHandler = new BeatmapRequestHandler();
-
             AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
 
             AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = 2).WaitSafely());
@@ -44,7 +49,11 @@ public override void SetUpSteps()
                     }).WaitSafely();
                 }
             });
+        }
 
+        [Test]
+        public void TestBasic()
+        {
             AddWaitStep("wait", 15);
 
             AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(2, hand => hand[0]).WaitSafely());
@@ -54,5 +63,29 @@ public override void SetUpSteps()
                 BeatmapID = requestHandler.Beatmaps[0].OnlineID
             }).WaitSafely());
         }
+
+        [Test]
+        public void TestPickPreviewPlayedOnOpponentPick()
+        {
+            RankedPlayCard.SongPreviewContainer? originalPreview = null;
+
+            AddStep("hover first card",
+                () => InputManager.MoveMouseTo(this.ChildrenOfType<PlayerHandOfCards>().Single().Cards
+                                                   .First(c => c.Item.PlaylistItem.Value != null && c.Item.PlaylistItem.Value.BeatmapID != requestHandler.Beatmaps[0].OnlineID)));
+            AddUntilStep("preview playing", () => originalPreview = this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().FirstOrDefault(p => p.IsRunning), () => Is.Not.Null);
+
+            AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(2, hand => hand[0]).WaitSafely());
+            AddStep("reveal card", () => MultiplayerClient.RankedPlayRevealUserCard(2, hand => hand[0], new MultiplayerPlaylistItem
+            {
+                ID = 0,
+                BeatmapID = requestHandler.Beatmaps[0].OnlineID
+            }).WaitSafely());
+
+            AddUntilStep("wait for original preview stopped", () => originalPreview?.IsRunning, () => Is.False);
+
+            AddUntilStep("preview playing is opponent's pick",
+                () => ((RankedPlayCard)this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().SingleOrDefault(p => p.IsRunning)?.Parent!).Item.PlaylistItem.Value?.BeatmapID,
+                () => Is.EqualTo(requestHandler.Beatmaps[0].OnlineID));
+        }
     }
 }

```

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…37477)

- Merge upstream ppy/osu master through commit a4f79f7 (PR ppy#37477:
  ranked play single-thread audio fix, supersedes/reverts ppy#37463).
- Resolve conflict in RankedPlayCard.SongPreview.cs in favour of our
  fork's existing late-bind approach (Enabled/CardHovered subscribed
  inside LoadComponentAsync callback so they never fire pre-load).
- Pick up the new TestPreviewStopsOnEnteringGameplay regression test.

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…Activity SIGSEGV

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/a8a91f1f-e013-42d6-a7e3-f0d1accb9b89

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/a8a91f1f-e013-42d6-a7e3-f0d1accb9b89

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
@winnerspiros winnerspiros marked this pull request as ready for review April 22, 2026 19:51
Copilot AI review requested due to automatic review settings April 22, 2026 19:51
@winnerspiros winnerspiros merged commit 5ee1984 into master Apr 22, 2026
14 of 16 checks passed
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Apr 22, 2026

Important

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

Gitar

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the project to newer ppy.osu.Framework packages and applies an Android Surface lifetime fix intended to prevent SDLActivity JNI crashes after surface loss/recreation, plus a regression test around ranked-play song preview stopping when entering gameplay.

Changes:

  • Bump ppy.osu.Framework / ppy.osu.Framework.Android / ppy.osu.Framework.iOS to 2026.422.1.
  • Root the Android Surface managed peer (heldSurface) alongside the JNI global ref, and serialize create/destroy to avoid releasing the Java peer while native code still holds a handle.
  • Add a visual test asserting ranked-play song previews stop once gameplay is requested; refresh local-packages documentation version reference.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
osu.iOS.props Bumps ppy.osu.Framework.iOS to 2026.422.1.
osu.Android.props Bumps ppy.osu.Framework.Android to 2026.422.1.
osu.Game/osu.Game.csproj Bumps ppy.osu.Framework to 2026.422.1 and updates the pinned-version comment.
osu.Android/OsuGameActivity.cs Keeps the managed Surface peer alive and tightens create/destroy ordering to prevent JNI crashes.
osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayScreen.cs Adds TestPreviewStopsOnEnteringGameplay regression coverage for song preview shutdown.
local-packages/README.md Updates the referenced framework version to 2026.422.1.

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

BeatmapID = requestHandler.Beatmaps[i2].OnlineID
}).WaitSafely());
}

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.

5 participants