Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion local-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ It is wired up as a NuGet source in the repo-root `NuGet.Config`:

| Package | Version | Why vendored |
|---------------------------------|------------------------|--------------|
| `ppy.Veldrid.SPIRV` | `1.0.15-gb268bf39ea` | This fork build (from <https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0>) ships `runtimes/android-arm64/native/libveldrid-spirv.so` aligned to **16 KB pages**, which is required for Android 16+. The version published on nuget.org (`1.0.15-gb66ebf81d2`) is 4 KB-aligned and triggers a build warning when packaging the APK. The version is referenced by `ppy.osu.Framework 2026.421.1` and re-pinned explicitly in `osu.Game/osu.Game.csproj` so resolution is deterministic. |
| `ppy.Veldrid.SPIRV` | `1.0.15-gb268bf39ea` | This fork build (from <https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0>) ships `runtimes/android-arm64/native/libveldrid-spirv.so` aligned to **16 KB pages**, which is required for Android 16+. The version published on nuget.org (`1.0.15-gb66ebf81d2`) is 4 KB-aligned and triggers a build warning when packaging the APK. The version is referenced by `ppy.osu.Framework 2026.422.1` and re-pinned explicitly in `osu.Game/osu.Game.csproj` so resolution is deterministic. |

## Updating

Expand Down
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.420.2" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.422.1" />
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.
Expand Down
71 changes: 60 additions & 11 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,32 @@ await Task.WhenAll(uris.Select(async uri =>
});

private readonly System.Threading.ManualResetEventSlim surfaceEvent = new System.Threading.ManualResetEventSlim(false);

// Hold both the JNI global ref AND the managed Surface peer alive against the
// SurfaceView lifecycle. The global ref alone is NOT enough — .NET-for-Android
// tracks managed peers separately, and once the local `Surface` returned by
// `holder.Surface` becomes GC-eligible (i.e. once SurfaceCreated returns), the
// peer's finaliser will release the underlying Java Surface even though we still
// hold a global ref to its handle. The next time the SDL thread tries to use that
// handle through JNI we crash with SIGSEGV inside libart.so on the SDLActivity
// thread (see native_crash.log). Storing the wrapper in a managed field roots the
// peer for the SurfaceView's entire lifetime.
//
// SurfaceCreated and SurfaceDestroyed are serialised against each other via
// `surfaceLock` so the SDL/Veldrid backend can never observe a half-torn-down
// state (e.g. global ref present but managed peer already released, or vice
// versa). The handle reader uses Volatile.Read for an unlocked fast path on hot
// call sites and a locked slow path is unnecessary because all writes happen
// under the lock and Interlocked.Exchange / Volatile.Write are release barriers.
private readonly object surfaceLock = new object();
private global::Android.Views.Surface? heldSurface;
private IntPtr surfaceGlobalRef;

public IntPtr GetSurfaceGlobalRef()
{
if (!surfaceEvent.Wait(5000))
Debug.WriteLine("[osu!] Warning: Wait for surface timed out");
return surfaceGlobalRef;
return System.Threading.Volatile.Read(ref surfaceGlobalRef);
}

public SurfaceView? GetSurface() => findSurfaceView(Window?.DecorView);
Expand All @@ -387,19 +406,33 @@ public IntPtr GetSurfaceGlobalRef()
public void SurfaceCreated(ISurfaceHolder holder)
{
var surface = holder.Surface;
if (surface != null && surface.IsValid)
{
IntPtr handle = surface.Handle;
if (handle == IntPtr.Zero) return;
if (surface == null || !surface.IsValid)
return;

IntPtr handle = surface.Handle;
if (handle == IntPtr.Zero)
return;

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

lock (surfaceLock)
{
// Establish the new managed root BEFORE publishing the new global ref so
// that any reader that observes the new ref already has its managed peer
// pinned. Then atomically swap in the new ref and release the previous one.
var oldHeld = heldSurface;
heldSurface = surface;

// Atomically swap the old reference to prevent race with SurfaceDestroyed.
IntPtr oldRef = System.Threading.Interlocked.Exchange(ref surfaceGlobalRef, newRef);

if (oldRef != IntPtr.Zero)
global::Android.Runtime.JNIEnv.DeleteGlobalRef(oldRef);

// Drop the previous managed root only AFTER its global ref is gone, so
// there is no window where consumers can hold a stale global ref pointing
// into a Java peer whose .NET wrapper has been disposed.
oldHeld?.Dispose();

Debug.WriteLine("[osu!] Native surface JNI global reference created (waiting for SurfaceChanged for signal)");
}
}
Expand All @@ -420,12 +453,28 @@ public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Forma

public void SurfaceDestroyed(ISurfaceHolder holder)
{
IntPtr oldRef = System.Threading.Interlocked.Exchange(ref surfaceGlobalRef, IntPtr.Zero);
// Block any concurrent SurfaceCreated so the SDL/Veldrid thread can never
// observe a partial state where the global ref has been freed but the
// managed peer is still alive (or the inverse).
lock (surfaceLock)
{
// Reset the readiness signal first so any waiter blocks until a new
// surface is published, rather than racing with the teardown below.
surfaceEvent.Reset();

// Release the global ref BEFORE dropping the managed root, never the
// other way around: once the .NET wrapper is disposed the underlying
// Java Surface may be released, and any subsequent JNI use of an
// outstanding global ref to that handle would segfault. Order here
// mirrors the inverse of SurfaceCreated.
IntPtr oldRef = System.Threading.Interlocked.Exchange(ref surfaceGlobalRef, IntPtr.Zero);

if (oldRef != IntPtr.Zero)
global::Android.Runtime.JNIEnv.DeleteGlobalRef(oldRef);
if (oldRef != IntPtr.Zero)
global::Android.Runtime.JNIEnv.DeleteGlobalRef(oldRef);

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

public override void OnConfigurationChanged(Configuration newConfig)
Expand Down
38 changes: 38 additions & 0 deletions osu.Game.Tests/Visual/RankedPlay/TestSceneRankedPlayScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
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;
using osuTK.Input;

Expand Down Expand Up @@ -214,5 +216,41 @@ public void TestHealthChange()
AddWaitStep("wait", 5);
AddStep("change player 2 health", () => MultiplayerClient.RankedPlayChangeUserState(2, state => state.Life = 250_000).WaitSafely());
}

[Test]
public void TestPreviewStopsOnEnteringGameplay()
{
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 2 }));

AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));

var requestHandler = new BeatmapRequestHandler();

AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);

AddStep("set play phase", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = 1001).WaitSafely());

for (int i = 0; i < 3; i++)
{
int i2 = i;
AddStep("reveal card", () => MultiplayerClient.RankedPlayRevealCard(hand => hand[i2], new MultiplayerPlaylistItem
{
ID = i2,
BeatmapID = requestHandler.Beatmaps[i2].OnlineID
}).WaitSafely());
}

AddStep("hover first card", () => InputManager.MoveMouseTo(this.ChildrenOfType<PlayerHandOfCards>().Single().Cards.First()));
AddUntilStep("preview playing", () => this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().Any(p => p.IsRunning), () => Is.True);

AddWaitStep("wait", 1);
AddStep("play beatmap", () => MultiplayerClient.PlayUserCard(1001, hand => hand[0]).WaitSafely());

AddStep("set warmup", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.GameplayWarmup).WaitSafely());
AddUntilStep("preview running", () => this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().Any(p => p.IsRunning), () => Is.True);

AddStep("load requested", () => ((IMultiplayerClient)MultiplayerClient).LoadRequested());
AddUntilStep("preview stopped", () => this.ChildrenOfType<RankedPlayCard.SongPreviewContainer>().Any(p => p.IsRunning), () => Is.False);
}
}
}
4 changes: 2 additions & 2 deletions osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.420.2" />
<PackageReference Include="ppy.osu.Framework" Version="2026.422.1" />
<!--
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
`ppy.osu.Framework 2026.420.2` was compiled against. This version is the only
`ppy.osu.Framework 2026.422.1` was compiled against. This version is the only
one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB
pages (required by Android 16+). It lives only as a release asset on
<https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0> and is vendored
Expand Down
2 changes: 1 addition & 1 deletion osu.iOS.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.420.2" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.422.1" />
</ItemGroup>
</Project>
Loading