Skip to content

Commit 0389f1e

Browse files
authored
Merge pull request #103 from winnerspiros/copilot/fix-android-apk-crash
Fix Android startup crash on Samsung devices by deferring native type loading
2 parents 3ff3b61 + 147a76c commit 0389f1e

1 file changed

Lines changed: 62 additions & 17 deletions

File tree

osu.Android/OsuGameAndroid.cs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Linq;
6+
using System.Runtime.CompilerServices;
67
using Android.App;
78
using Android.Content.PM;
89
using Android.Views;
@@ -38,10 +39,15 @@ public partial class OsuGameAndroid : OsuGame
3839
private readonly BindableDouble audioOffset = new BindableDouble();
3940

4041
/// <summary>
41-
/// Native bridge manager — kept as a separate type so OboeAudioBridge / VulkanProbe
42-
/// types are never loaded during OsuGameAndroid class initialisation.
42+
/// Boxed reference to the native bridge manager.
43+
/// Declared as <c>object?</c> so that the runtime never resolves the concrete
44+
/// AndroidNativeBridgeManager type (and its P/Invoke field types) during
45+
/// OsuGameAndroid class initialisation — which would trigger
46+
/// NativeLibrary.TryLoad before the framework is ready and crash on some
47+
/// Samsung devices.
48+
/// All access goes through [NoInlining] helpers below.
4349
/// </summary>
44-
private AndroidNativeBridgeManager? nativeBridges;
50+
private object? nativeBridges;
4551

4652
public OsuGameAndroid(OsuGameActivity activity)
4753
: base(null)
@@ -122,8 +128,7 @@ protected override void LoadComplete()
122128
{
123129
if (e.NewValue)
124130
{
125-
nativeBridges ??= new AndroidNativeBridgeManager();
126-
nativeBridges.StartOboeBridge(Scheduler, latency =>
131+
startOboeBridge(latency =>
127132
{
128133
// Only auto-suggest when the user hasn't already configured a manual offset.
129134
if (Math.Abs(audioOffset.Value) >= 0.01)
@@ -136,7 +141,7 @@ protected override void LoadComplete()
136141
}
137142
else
138143
{
139-
nativeBridges?.StopOboeBridge();
144+
stopOboeBridge();
140145
}
141146
}
142147
catch (Exception ex)
@@ -150,14 +155,9 @@ protected override void LoadComplete()
150155
try
151156
{
152157
if (e.NewValue)
153-
{
154-
nativeBridges ??= new AndroidNativeBridgeManager();
155-
nativeBridges.StartVulkanProbe();
156-
}
158+
startVulkanProbe();
157159
else
158-
{
159-
nativeBridges?.StopVulkanProbe();
160-
}
160+
stopVulkanProbe();
161161
}
162162
catch (Exception ex)
163163
{
@@ -244,7 +244,54 @@ private void selectHighestRefreshRate()
244244
/// </summary>
245245
public double GetMeasuredAudioLatencyMs()
246246
{
247-
return nativeBridges?.GetMeasuredAudioLatencyMs() ?? -1;
247+
return getMeasuredAudioLatencyFromBridge();
248+
}
249+
250+
// ── Native bridge helpers ──────────────────────────────────────────
251+
// Every method below is [NoInlining] so that AndroidNativeBridgeManager
252+
// (and its P/Invoke field types) are never resolved until explicitly called.
253+
254+
[MethodImpl(MethodImplOptions.NoInlining)]
255+
private void startOboeBridge(Action<double> onLatencyMeasured)
256+
{
257+
nativeBridges ??= new AndroidNativeBridgeManager();
258+
259+
if (nativeBridges is AndroidNativeBridgeManager mgr)
260+
mgr.StartOboeBridge(Scheduler, onLatencyMeasured);
261+
}
262+
263+
[MethodImpl(MethodImplOptions.NoInlining)]
264+
private void stopOboeBridge()
265+
{
266+
(nativeBridges as AndroidNativeBridgeManager)?.StopOboeBridge();
267+
}
268+
269+
[MethodImpl(MethodImplOptions.NoInlining)]
270+
private void startVulkanProbe()
271+
{
272+
nativeBridges ??= new AndroidNativeBridgeManager();
273+
274+
if (nativeBridges is AndroidNativeBridgeManager mgr)
275+
mgr.StartVulkanProbe();
276+
}
277+
278+
[MethodImpl(MethodImplOptions.NoInlining)]
279+
private void stopVulkanProbe()
280+
{
281+
(nativeBridges as AndroidNativeBridgeManager)?.StopVulkanProbe();
282+
}
283+
284+
[MethodImpl(MethodImplOptions.NoInlining)]
285+
private double getMeasuredAudioLatencyFromBridge()
286+
{
287+
return (nativeBridges as AndroidNativeBridgeManager)?.GetMeasuredAudioLatencyMs() ?? -1;
288+
}
289+
290+
[MethodImpl(MethodImplOptions.NoInlining)]
291+
private void disposeNativeBridges()
292+
{
293+
(nativeBridges as AndroidNativeBridgeManager)?.Dispose();
294+
nativeBridges = null;
248295
}
249296

250297
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
@@ -303,9 +350,7 @@ public override void SetHost(GameHost host)
303350
protected override void Dispose(bool isDisposing)
304351
{
305352
base.Dispose(isDisposing);
306-
307-
nativeBridges?.Dispose();
308-
nativeBridges = null;
353+
disposeNativeBridges();
309354
}
310355

311356
private class AndroidBatteryInfo : BatteryInfo

0 commit comments

Comments
 (0)