|
7 | 7 | using Android.Content.PM; |
8 | 8 | using Android.Views; |
9 | 9 | using Microsoft.Maui.Devices; |
10 | | -using osu.Android.Native; |
11 | 10 | using osu.Framework.Allocation; |
12 | 11 | using osu.Framework.Bindables; |
13 | 12 | using osu.Framework.Development; |
@@ -38,8 +37,11 @@ public partial class OsuGameAndroid : OsuGame |
38 | 37 | private readonly Bindable<bool> vulkanProbeEnabled = new Bindable<bool>(); |
39 | 38 | private readonly BindableDouble audioOffset = new BindableDouble(); |
40 | 39 |
|
41 | | - private OboeAudioBridge? oboeBridge; |
42 | | - private VulkanProbe? vulkanProbe; |
| 40 | + /// <summary> |
| 41 | + /// Native bridge manager — kept as a separate type so OboeAudioBridge / VulkanProbe |
| 42 | + /// types are never loaded during OsuGameAndroid class initialisation. |
| 43 | + /// </summary> |
| 44 | + private AndroidNativeBridgeManager? nativeBridges; |
43 | 45 |
|
44 | 46 | public OsuGameAndroid(OsuGameActivity activity) |
45 | 47 | : base(null) |
@@ -116,18 +118,49 @@ protected override void LoadComplete() |
116 | 118 |
|
117 | 119 | lowLatencyAudio.BindValueChanged(e => |
118 | 120 | { |
119 | | - if (e.NewValue) |
120 | | - startOboeBridge(); |
121 | | - else |
122 | | - stopOboeBridge(); |
| 121 | + try |
| 122 | + { |
| 123 | + nativeBridges ??= new AndroidNativeBridgeManager(); |
| 124 | + |
| 125 | + if (e.NewValue) |
| 126 | + { |
| 127 | + nativeBridges.StartOboeBridge(Scheduler, latency => |
| 128 | + { |
| 129 | + // Only auto-suggest when the user hasn't already configured a manual offset. |
| 130 | + if (Math.Abs(audioOffset.Value) >= 0.01) |
| 131 | + return; |
| 132 | + |
| 133 | + double suggested = Math.Clamp(-latency, audioOffset.MinValue, audioOffset.MaxValue); |
| 134 | + audioOffset.Value = suggested; |
| 135 | + Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)"); |
| 136 | + }); |
| 137 | + } |
| 138 | + else |
| 139 | + { |
| 140 | + nativeBridges.StopOboeBridge(); |
| 141 | + } |
| 142 | + } |
| 143 | + catch (Exception ex) |
| 144 | + { |
| 145 | + Debug.WriteLine($"[osu!] Failed to toggle Oboe bridge: {ex.Message}"); |
| 146 | + } |
123 | 147 | }, true); |
124 | 148 |
|
125 | 149 | vulkanProbeEnabled.BindValueChanged(e => |
126 | 150 | { |
127 | | - if (e.NewValue) |
128 | | - startVulkanProbe(); |
129 | | - else |
130 | | - stopVulkanProbe(); |
| 151 | + try |
| 152 | + { |
| 153 | + nativeBridges ??= new AndroidNativeBridgeManager(); |
| 154 | + |
| 155 | + if (e.NewValue) |
| 156 | + nativeBridges.StartVulkanProbe(); |
| 157 | + else |
| 158 | + nativeBridges.StopVulkanProbe(); |
| 159 | + } |
| 160 | + catch (Exception ex) |
| 161 | + { |
| 162 | + Debug.WriteLine($"[osu!] Failed to toggle Vulkan probe: {ex.Message}"); |
| 163 | + } |
131 | 164 | }, true); |
132 | 165 |
|
133 | 166 | // Apply unbuffered touch dispatch (deferred from Activity lifecycle to avoid early crash). |
@@ -203,137 +236,13 @@ private void selectHighestRefreshRate() |
203 | 236 | } |
204 | 237 | } |
205 | 238 |
|
206 | | - private void startOboeBridge() |
207 | | - { |
208 | | - if (oboeBridge != null) return; |
209 | | - |
210 | | - try |
211 | | - { |
212 | | - oboeBridge = OboeAudioBridge.Create(); |
213 | | - |
214 | | - if (oboeBridge != null) |
215 | | - { |
216 | | - bool started = oboeBridge.Start(); |
217 | | - |
218 | | - if (started) |
219 | | - { |
220 | | - // Log basic stream info immediately. |
221 | | - logOboeInfo(); |
222 | | - |
223 | | - // Latency is measured asynchronously by the audio callback. |
224 | | - // Schedule a check after a short warm-up period to get a stable reading |
225 | | - // and apply the auto-suggested audio offset if appropriate. |
226 | | - Scheduler.AddDelayed(applyMeasuredLatencyOffset, 2000); |
227 | | - } |
228 | | - else |
229 | | - { |
230 | | - Debug.WriteLine("[osu!] Oboe bridge created but failed to start"); |
231 | | - } |
232 | | - } |
233 | | - } |
234 | | - catch (Exception e) |
235 | | - { |
236 | | - Debug.WriteLine($"[osu!] Oboe bridge init failed: {e.Message}"); |
237 | | - } |
238 | | - } |
239 | | - |
240 | | - private void stopOboeBridge() |
241 | | - { |
242 | | - oboeBridge?.Dispose(); |
243 | | - oboeBridge = null; |
244 | | - Debug.WriteLine("[osu!] Oboe bridge stopped by user setting"); |
245 | | - } |
246 | | - |
247 | | - private void startVulkanProbe() |
248 | | - { |
249 | | - if (vulkanProbe != null) return; |
250 | | - |
251 | | - try |
252 | | - { |
253 | | - vulkanProbe = VulkanProbe.Create(); |
254 | | - |
255 | | - if (vulkanProbe != null) |
256 | | - { |
257 | | - logVulkanInfo(); |
258 | | - } |
259 | | - } |
260 | | - catch (Exception e) |
261 | | - { |
262 | | - Debug.WriteLine($"[osu!] Vulkan probe init failed: {e.Message}"); |
263 | | - } |
264 | | - } |
265 | | - |
266 | | - private void stopVulkanProbe() |
267 | | - { |
268 | | - vulkanProbe?.Dispose(); |
269 | | - vulkanProbe = null; |
270 | | - Debug.WriteLine("[osu!] Vulkan probe stopped by user setting"); |
271 | | - } |
272 | | - |
273 | | - private void logVulkanInfo() |
274 | | - { |
275 | | - if (vulkanProbe == null) return; |
276 | | - |
277 | | - int ver = vulkanProbe.ApiVersion; |
278 | | - int major = (ver >> 22) & 0x3FF; |
279 | | - int minor = (ver >> 12) & 0x3FF; |
280 | | - int patch = ver & 0xFFF; |
281 | | - |
282 | | - Debug.WriteLine($"[osu!] Vulkan GPU: available={vulkanProbe.IsAvailable}, " |
283 | | - + $"API={major}.{minor}.{patch}, " |
284 | | - + $"swapchain={vulkanProbe.SupportsSwapchain}, " |
285 | | - + $"mailbox={vulkanProbe.SupportsMailboxPresentMode}, " |
286 | | - + $"VRAM={vulkanProbe.DeviceLocalMemoryMB}MB, " |
287 | | - + $"queueFamilies={vulkanProbe.QueueFamilyCount}, " |
288 | | - + $"dedicatedCompute={vulkanProbe.HasDedicatedComputeQueue}, " |
289 | | - + $"dedicatedTransfer={vulkanProbe.HasDedicatedTransferQueue}"); |
290 | | - } |
291 | | - |
292 | | - private void logOboeInfo() |
293 | | - { |
294 | | - if (oboeBridge == null) return; |
295 | | - |
296 | | - Debug.WriteLine($"[osu!] Oboe audio: active={oboeBridge.IsActive}, " |
297 | | - + $"api={(oboeBridge.IsAAudio ? "AAudio" : "OpenSLES")}, " |
298 | | - + $"sampleRate={oboeBridge.SampleRate}Hz, " |
299 | | - + $"burst={oboeBridge.FramesPerBurst}frames, " |
300 | | - + $"bufferSize={oboeBridge.BufferSizeInFrames}frames"); |
301 | | - } |
302 | | - |
303 | | - /// <summary> |
304 | | - /// Called after a warm-up delay to read the stable measured latency and apply it |
305 | | - /// as an auto-suggested audio offset when the user hasn't set a manual value. |
306 | | - /// </summary> |
307 | | - private void applyMeasuredLatencyOffset() |
308 | | - { |
309 | | - if (oboeBridge == null) return; |
310 | | - |
311 | | - double latency = oboeBridge.GetOutputLatencyMs(); |
312 | | - |
313 | | - Debug.WriteLine($"[osu!] Oboe measured latency after warm-up: {latency:F1}ms"); |
314 | | - |
315 | | - if (latency <= 0) |
316 | | - return; |
317 | | - |
318 | | - // Only auto-suggest when the user hasn't already configured a manual offset. |
319 | | - // Use a small epsilon to safely compare against the default value of 0. |
320 | | - if (Math.Abs(audioOffset.Value) >= 0.01) |
321 | | - return; |
322 | | - |
323 | | - // The audio offset compensates for hardware output delay: if audio arrives |
324 | | - // 20 ms late, we need to set the offset to -20 ms so osu! plays notes earlier. |
325 | | - double suggested = Math.Clamp(-latency, audioOffset.MinValue, audioOffset.MaxValue); |
326 | | - audioOffset.Value = suggested; |
327 | | - Debug.WriteLine($"[osu!] Audio offset auto-suggested: {suggested:F1}ms (hardware latency={latency:F1}ms)"); |
328 | | - } |
329 | | - |
330 | 239 | /// <summary> |
331 | 240 | /// Returns the measured audio output latency in milliseconds via the Oboe bridge, |
332 | 241 | /// or -1 if unavailable. Can be used to auto-suggest audio offset calibration. |
333 | 242 | /// </summary> |
334 | 243 | public double GetMeasuredAudioLatencyMs() |
335 | 244 | { |
336 | | - return oboeBridge?.GetOutputLatencyMs() ?? -1; |
| 245 | + return nativeBridges?.GetMeasuredAudioLatencyMs() ?? -1; |
337 | 246 | } |
338 | 247 |
|
339 | 248 | protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen) |
@@ -393,11 +302,8 @@ protected override void Dispose(bool isDisposing) |
393 | 302 | { |
394 | 303 | base.Dispose(isDisposing); |
395 | 304 |
|
396 | | - oboeBridge?.Dispose(); |
397 | | - oboeBridge = null; |
398 | | - |
399 | | - vulkanProbe?.Dispose(); |
400 | | - vulkanProbe = null; |
| 305 | + nativeBridges?.Dispose(); |
| 306 | + nativeBridges = null; |
401 | 307 | } |
402 | 308 |
|
403 | 309 | private class AndroidBatteryInfo : BatteryInfo |
|
0 commit comments