Skip to content

Commit 1e952cc

Browse files
Copilotwinnerspiros
andcommitted
Fix Android startup crash on Samsung devices by deferring native type loading
Change the nativeBridges field from AndroidNativeBridgeManager? to object? so the runtime never resolves the concrete type (and its P/Invoke field types OboeAudioBridge/VulkanProbe) during OsuGameAndroid class initialisation. All access to AndroidNativeBridgeManager is moved to [NoInlining] helper methods, ensuring the type is only loaded when a native feature is explicitly enabled by the user. Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/95598d2f-be81-4823-9192-311690d7d7f2 Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 2be6688 commit 1e952cc

1 file changed

Lines changed: 58 additions & 17 deletions

File tree

osu.Android/OsuGameAndroid.cs

Lines changed: 58 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,50 @@ 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+
((AndroidNativeBridgeManager)nativeBridges).StartOboeBridge(Scheduler, onLatencyMeasured);
259+
}
260+
261+
[MethodImpl(MethodImplOptions.NoInlining)]
262+
private void stopOboeBridge()
263+
{
264+
(nativeBridges as AndroidNativeBridgeManager)?.StopOboeBridge();
265+
}
266+
267+
[MethodImpl(MethodImplOptions.NoInlining)]
268+
private void startVulkanProbe()
269+
{
270+
nativeBridges ??= new AndroidNativeBridgeManager();
271+
((AndroidNativeBridgeManager)nativeBridges).StartVulkanProbe();
272+
}
273+
274+
[MethodImpl(MethodImplOptions.NoInlining)]
275+
private void stopVulkanProbe()
276+
{
277+
(nativeBridges as AndroidNativeBridgeManager)?.StopVulkanProbe();
278+
}
279+
280+
[MethodImpl(MethodImplOptions.NoInlining)]
281+
private double getMeasuredAudioLatencyFromBridge()
282+
{
283+
return (nativeBridges as AndroidNativeBridgeManager)?.GetMeasuredAudioLatencyMs() ?? -1;
284+
}
285+
286+
[MethodImpl(MethodImplOptions.NoInlining)]
287+
private void disposeNativeBridges()
288+
{
289+
(nativeBridges as AndroidNativeBridgeManager)?.Dispose();
290+
nativeBridges = null;
248291
}
249292

250293
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
@@ -303,9 +346,7 @@ public override void SetHost(GameHost host)
303346
protected override void Dispose(bool isDisposing)
304347
{
305348
base.Dispose(isDisposing);
306-
307-
nativeBridges?.Dispose();
308-
nativeBridges = null;
349+
disposeNativeBridges();
309350
}
310351

311352
private class AndroidBatteryInfo : BatteryInfo

0 commit comments

Comments
 (0)