Skip to content

Commit ff042fd

Browse files
authored
Merge pull request #101 from winnerspiros/copilot/fix-app-crash-on-start
Fix Android startup crash: isolate native bridge types from eager class loading
2 parents bf100fb + 6eae29e commit ff042fd

4 files changed

Lines changed: 258 additions & 206 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using osu.Android.Native;
6+
using osu.Framework.Threading;
7+
using Debug = System.Diagnostics.Debug;
8+
9+
namespace osu.Android
10+
{
11+
/// <summary>
12+
/// Encapsulates all native bridge lifecycle management (Oboe audio, Vulkan probe).
13+
/// Kept in a SEPARATE class so that <see cref="OboeAudioBridge"/> and <see cref="VulkanProbe"/>
14+
/// types are only loaded by the runtime when this class is first accessed — NOT during
15+
/// <see cref="OsuGameAndroid"/> class initialization, which happens before the framework
16+
/// is ready and before native libraries are expected to be available.
17+
/// </summary>
18+
internal sealed class AndroidNativeBridgeManager : IDisposable
19+
{
20+
private OboeAudioBridge? oboeBridge;
21+
private VulkanProbe? vulkanProbe;
22+
private volatile bool disposed;
23+
24+
public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasured)
25+
{
26+
if (oboeBridge != null) return;
27+
28+
try
29+
{
30+
oboeBridge = OboeAudioBridge.Create();
31+
32+
if (oboeBridge != null)
33+
{
34+
bool started = oboeBridge.Start();
35+
36+
if (started)
37+
{
38+
logOboeInfo();
39+
40+
// Latency is measured asynchronously by the audio callback.
41+
// Schedule a check after a short warm-up period to get a stable reading.
42+
scheduler.AddDelayed(() =>
43+
{
44+
if (oboeBridge == null) return;
45+
46+
double latency = oboeBridge.GetOutputLatencyMs();
47+
Debug.WriteLine($"[osu!] Oboe measured latency after warm-up: {latency:F1}ms");
48+
49+
if (latency > 0)
50+
onLatencyMeasured(latency);
51+
}, 2000);
52+
}
53+
else
54+
{
55+
Debug.WriteLine("[osu!] Oboe bridge created but failed to start");
56+
}
57+
}
58+
}
59+
catch (Exception e)
60+
{
61+
Debug.WriteLine($"[osu!] Oboe bridge init failed: {e.Message}");
62+
}
63+
}
64+
65+
public void StopOboeBridge()
66+
{
67+
oboeBridge?.Dispose();
68+
oboeBridge = null;
69+
Debug.WriteLine("[osu!] Oboe bridge stopped by user setting");
70+
}
71+
72+
public void StartVulkanProbe()
73+
{
74+
if (vulkanProbe != null) return;
75+
76+
try
77+
{
78+
vulkanProbe = VulkanProbe.Create();
79+
80+
if (vulkanProbe != null)
81+
logVulkanInfo();
82+
}
83+
catch (Exception e)
84+
{
85+
Debug.WriteLine($"[osu!] Vulkan probe init failed: {e.Message}");
86+
}
87+
}
88+
89+
public void StopVulkanProbe()
90+
{
91+
vulkanProbe?.Dispose();
92+
vulkanProbe = null;
93+
Debug.WriteLine("[osu!] Vulkan probe stopped by user setting");
94+
}
95+
96+
/// <summary>
97+
/// Returns the measured audio output latency in milliseconds via the Oboe bridge,
98+
/// or -1 if unavailable.
99+
/// </summary>
100+
public double GetMeasuredAudioLatencyMs()
101+
{
102+
return oboeBridge?.GetOutputLatencyMs() ?? -1;
103+
}
104+
105+
private void logVulkanInfo()
106+
{
107+
if (vulkanProbe == null) return;
108+
109+
int ver = vulkanProbe.ApiVersion;
110+
int major = (ver >> 22) & 0x3FF;
111+
int minor = (ver >> 12) & 0x3FF;
112+
int patch = ver & 0xFFF;
113+
114+
Debug.WriteLine($"[osu!] Vulkan GPU: available={vulkanProbe.IsAvailable}, "
115+
+ $"API={major}.{minor}.{patch}, "
116+
+ $"swapchain={vulkanProbe.SupportsSwapchain}, "
117+
+ $"mailbox={vulkanProbe.SupportsMailboxPresentMode}, "
118+
+ $"VRAM={vulkanProbe.DeviceLocalMemoryMB}MB, "
119+
+ $"queueFamilies={vulkanProbe.QueueFamilyCount}, "
120+
+ $"dedicatedCompute={vulkanProbe.HasDedicatedComputeQueue}, "
121+
+ $"dedicatedTransfer={vulkanProbe.HasDedicatedTransferQueue}");
122+
}
123+
124+
private void logOboeInfo()
125+
{
126+
if (oboeBridge == null) return;
127+
128+
Debug.WriteLine($"[osu!] Oboe audio: active={oboeBridge.IsActive}, "
129+
+ $"api={(oboeBridge.IsAAudio ? "AAudio" : "OpenSLES")}, "
130+
+ $"sampleRate={oboeBridge.SampleRate}Hz, "
131+
+ $"burst={oboeBridge.FramesPerBurst}frames, "
132+
+ $"bufferSize={oboeBridge.BufferSizeInFrames}frames");
133+
}
134+
135+
public void Dispose()
136+
{
137+
if (disposed) return;
138+
139+
disposed = true;
140+
141+
oboeBridge?.Dispose();
142+
oboeBridge = null;
143+
144+
vulkanProbe?.Dispose();
145+
vulkanProbe = null;
146+
}
147+
}
148+
}

osu.Android/OsuGameActivity.cs

Lines changed: 3 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,6 @@ public OsuGameActivity()
6969
game = new OsuGameAndroid(this);
7070
}
7171

72-
protected override void OnStart()
73-
{
74-
base.OnStart();
75-
76-
try
77-
{
78-
// RequestUnbufferedDispatch(int sourceClass) requires API 31+.
79-
if (OperatingSystem.IsAndroidVersionAtLeast(31))
80-
Window?.DecorView?.RequestUnbufferedDispatch((int)InputSourceType.Touchscreen);
81-
}
82-
catch (Exception e)
83-
{
84-
Debug.WriteLine($"[osu!] Failed to request unbuffered touch dispatch: {e.Message}");
85-
}
86-
}
87-
8872
protected override void OnCreate(Bundle? savedInstanceState)
8973
{
9074
base.OnCreate(savedInstanceState);
@@ -117,86 +101,17 @@ protected override void OnCreate(Bundle? savedInstanceState)
117101
// Manually load them so that they can be loaded by RulesetStore.loadFromAppDomain.
118102
// REMEMBER to fully uninstall previous version every time when investigating this!
119103
// Don't forget osu.Game.Tests.Android too.
120-
Assembly.Load("osu.Game.Rulesets.Osu");
121-
Assembly.Load("osu.Game.Rulesets.Taiko");
122-
Assembly.Load("osu.Game.Rulesets.Catch");
123-
Assembly.Load("osu.Game.Rulesets.Mania");
124-
}
125-
126-
protected override void OnResume()
127-
{
128-
base.OnResume();
129-
130-
try
131-
{
132-
if (OperatingSystem.IsAndroidVersionAtLeast(31))
133-
{
134-
var gameManager = (GameManager?)GetSystemService(GameService);
135-
136-
if (gameManager != null)
137-
{
138-
bool isPerformanceMode = gameManager.GameMode == (int)GameMode.Performance;
139-
ApplyPerformanceOptimizations(isPerformanceMode);
140-
}
141-
}
142-
}
143-
catch (Exception e)
144-
{
145-
Debug.WriteLine($"[osu!] Failed to query game mode: {e.Message}");
146-
}
147-
}
148-
149-
/// <summary>
150-
/// Applies Android-level performance optimizations for low-latency gameplay.
151-
/// </summary>
152-
/// <param name="enabled">Whether to enable performance optimizations.</param>
153-
public void ApplyPerformanceOptimizations(bool enabled)
154-
{
155-
RunOnUiThread(() =>
104+
foreach (string asm in new[] { "osu.Game.Rulesets.Osu", "osu.Game.Rulesets.Taiko", "osu.Game.Rulesets.Catch", "osu.Game.Rulesets.Mania" })
156105
{
157106
try
158107
{
159-
Window?.SetSustainedPerformanceMode(enabled);
160-
161-
if (enabled)
162-
selectHighestRefreshRate();
108+
Assembly.Load(asm);
163109
}
164110
catch (Exception e)
165111
{
166-
Debug.WriteLine($"[osu!] Failed to apply performance optimizations: {e.Message}");
167-
}
168-
});
169-
}
170-
171-
private void selectHighestRefreshRate()
172-
{
173-
try
174-
{
175-
var display = WindowManager?.DefaultDisplay;
176-
177-
if (display == null || Window == null)
178-
return;
179-
180-
#pragma warning disable CA1422
181-
var modes = display.GetSupportedModes();
182-
#pragma warning restore CA1422
183-
184-
if (modes == null || modes.Length == 0)
185-
return;
186-
187-
var preferred = modes.OrderByDescending(m => m.RefreshRate).First();
188-
var layoutParams = Window.Attributes;
189-
190-
if (layoutParams != null)
191-
{
192-
layoutParams.PreferredDisplayModeId = preferred.ModeId;
193-
Window.Attributes = layoutParams;
112+
Debug.WriteLine($"[osu!] Failed to load ruleset assembly {asm}: {e.Message}");
194113
}
195114
}
196-
catch (Exception e)
197-
{
198-
Debug.WriteLine($"[osu!] Failed to select highest refresh rate: {e.Message}");
199-
}
200115
}
201116

202117
protected override void OnNewIntent(Intent? intent) => handleIntent(intent);

0 commit comments

Comments
 (0)