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
87 changes: 33 additions & 54 deletions osu.Android/AndroidNativeBridgeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,23 @@ namespace osu.Android
{
/// <summary>
/// Encapsulates all native bridge lifecycle management (Oboe audio, Vulkan probe).
/// Field types are declared as <c>object?</c> and all access is through
/// <c>[MethodImpl(NoInlining)]</c> helpers so that <see cref="OboeAudioBridge"/> and
/// <see cref="VulkanProbe"/> are only resolved by the runtime when their specific
/// feature is enabled — not when this class is loaded. This prevents Samsung-device
/// crashes caused by <c>NativeLibrary.TryLoad</c> being called during class
/// initialisation before the framework is ready.
/// </summary>
internal sealed class AndroidNativeBridgeManager : IDisposable
{
/// <summary>Boxed <see cref="OboeAudioBridge"/> — keeps the type out of class init.</summary>
private object? oboeBridge;

/// <summary>Boxed <see cref="VulkanProbe"/> — keeps the type out of class init.</summary>
private object? vulkanProbe;

private volatile bool disposed;

// ── Oboe ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[MethodImpl(MethodImplOptions.NoInlining)]
public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasured, IntPtr provider, int sampleRate = 0, Action<int>? onStarted = null)
{
if (oboeBridge != null) return;
if (oboeBridge != null)
{
Debug.WriteLine("[osu!] Oboe bridge already started, ignoring request");
return;
}

Debug.WriteLine($"[osu!] Starting Oboe bridge (sampleRate={sampleRate}, hasProvider={provider != IntPtr.Zero})");

try
{
Expand All @@ -50,6 +44,7 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure

if (started)
{
Debug.WriteLine("[osu!] Oboe bridge started successfully");
logOboeInfo(bridge);

onStarted?.Invoke(bridge.SampleRate);
Expand All @@ -66,22 +61,27 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure
}
else
{
Debug.WriteLine("[osu!] Oboe bridge created but failed to start");
Debug.WriteLine("[osu!] Oboe bridge created but failed to start (Start() returned false)");
}
}
else
{
Debug.WriteLine("[osu!] Oboe bridge creation failed (Create() returned null)");
}
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Oboe bridge init failed: {e.Message}");
Debug.WriteLine($"[osu!] Oboe bridge init failed with exception: {e.Message}");
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
public void StopOboeBridge()
{
Debug.WriteLine("[osu!] Stopping Oboe bridge...");
(oboeBridge as OboeAudioBridge)?.Dispose();
oboeBridge = null;
Debug.WriteLine("[osu!] Oboe bridge stopped by user setting");
Debug.WriteLine("[osu!] Oboe bridge stopped");
}

[MethodImpl(MethodImplOptions.NoInlining)]
Expand All @@ -96,18 +96,19 @@ public string GetOboeStatus()
if (oboeBridge is not OboeAudioBridge bridge) return string.Empty;
return $"{(bridge.IsAAudio ? "AAudio" : "OpenSLES")} [{(bridge.IsMMap ? "MMAP" : "Legacy")}]";
}

public double GetMeasuredAudioLatencyMs()
{
return (oboeBridge as OboeAudioBridge)?.GetOutputLatencyMs() ?? -1;
}

// ── Vulkan ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[MethodImpl(MethodImplOptions.NoInlining)]
public void StartVulkanProbe()
{
if (vulkanProbe != null) return;

Debug.WriteLine("[osu!] Starting Vulkan probe...");

try
{
var probe = VulkanProbe.Create();
Expand All @@ -117,6 +118,10 @@ public void StartVulkanProbe()
vulkanProbe = probe;
logVulkanInfo(probe);
}
else
{
Debug.WriteLine("[osu!] Vulkan probe creation failed (Create() returned null)");
}
}
catch (Exception e)
{
Expand All @@ -128,15 +133,14 @@ public void StartVulkanProbe()
public bool IsVulkanRecommended() => (vulkanProbe as VulkanProbe)?.IsRecommended ?? false;

public bool IsVulkanAvailable() => (vulkanProbe as VulkanProbe)?.IsAvailable ?? (vulkanProbe != null);

public void StopVulkanProbe()
{
(vulkanProbe as VulkanProbe)?.Dispose();
vulkanProbe = null;
Debug.WriteLine("[osu!] Vulkan probe stopped by user setting");
Debug.WriteLine("[osu!] Vulkan probe stopped");
}

// ── Logging ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[MethodImpl(MethodImplOptions.NoInlining)]
private static void logVulkanInfo(VulkanProbe probe)
{
Expand All @@ -145,59 +149,34 @@ private static void logVulkanInfo(VulkanProbe probe)
int minor = (ver >> 12) & 0x3FF;
int patch = ver & 0xFFF;

Debug.WriteLine($"[osu!] Vulkan GPU: available={probe.IsAvailable}, "
Debug.WriteLine($"[osu!] Vulkan GPU: {probe.DeviceLocalMemoryMB}MB, "
+ $"API={major}.{minor}.{patch}, "
+ $"swapchain={probe.SupportsSwapchain}, "
+ $"mailbox={probe.SupportsMailboxPresentMode}, "
+ $"VRAM={probe.DeviceLocalMemoryMB}MB, "
+ $"queueFamilies={probe.QueueFamilyCount}, "
+ $"dedicatedCompute={probe.HasDedicatedComputeQueue}, "
+ $"dedicatedTransfer={probe.HasDedicatedTransferQueue}, "
+ $"vk1.3={probe.MeetsVulkan13}, "
+ $"dynamicRendering={probe.SupportsDynamicRendering}, "
+ $"synchronization2={probe.SupportsSynchronization2}");
+ $"gpl={probe.SupportsGraphicsPipelineLibrary}, "
+ $"shaderObj={probe.SupportsShaderObject}");
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void logOboeInfo(OboeAudioBridge bridge)
{
Debug.WriteLine($"[osu!] Oboe audio: active={bridge.IsActive}, "
Debug.WriteLine($"[osu!] Oboe audio: {bridge.SampleRate}Hz, "
+ $"api={(bridge.IsAAudio ? "AAudio" : "OpenSLES")}, "
+ $"mmap={bridge.IsMMap}, "
+ $"sampleRate={bridge.SampleRate}Hz, "
+ $"burst={bridge.FramesPerBurst}frames, "
+ $"bufferSize={bridge.BufferSizeInFrames}frames");
+ $"burst={bridge.FramesPerBurst}, "
+ $"buffer={bridge.BufferSizeInFrames}");
}

// ── Cleanup ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[MethodImpl(MethodImplOptions.NoInlining)]
public void Dispose()
{
if (disposed) return;

disposed = true;

try
{
(oboeBridge as OboeAudioBridge)?.Dispose();
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Oboe dispose failed: {e.Message}");
}

try { (oboeBridge as OboeAudioBridge)?.Dispose(); } catch { }
oboeBridge = null;

try
{
(vulkanProbe as VulkanProbe)?.Dispose();
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] Vulkan dispose failed: {e.Message}");
}

try { (vulkanProbe as VulkanProbe)?.Dispose(); } catch { }
vulkanProbe = null;
}
}
Expand Down
Loading
Loading