22// See the LICENCE file in the repository root for full licence text.
33
44using System ;
5+ using System . Runtime . CompilerServices ;
56using osu . Android . Native ;
67using osu . Framework . Threading ;
78using Debug = System . Diagnostics . Debug ;
@@ -10,40 +11,48 @@ namespace osu.Android
1011{
1112 /// <summary>
1213 /// 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.
14+ /// Field types are declared as <c>object?</c> and all access is through
15+ /// <c>[MethodImpl(NoInlining)]</c> helpers so that <see cref="OboeAudioBridge"/> and
16+ /// <see cref="VulkanProbe"/> are only resolved by the runtime when their specific
17+ /// feature is enabled — not when this class is loaded. This prevents Samsung-device
18+ /// crashes caused by <c>NativeLibrary.TryLoad</c> being called during class
19+ /// initialisation before the framework is ready.
1720 /// </summary>
1821 internal sealed class AndroidNativeBridgeManager : IDisposable
1922 {
20- private OboeAudioBridge ? oboeBridge ;
21- private VulkanProbe ? vulkanProbe ;
23+ /// <summary>Boxed <see cref="OboeAudioBridge"/> — keeps the type out of class init.</summary>
24+ private object ? oboeBridge ;
25+
26+ /// <summary>Boxed <see cref="VulkanProbe"/> — keeps the type out of class init.</summary>
27+ private object ? vulkanProbe ;
28+
2229 private volatile bool disposed ;
2330
31+ // ── Oboe ───────────────────────────────────────────────────────────
32+
33+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
2434 public void StartOboeBridge ( Scheduler scheduler , Action < double > onLatencyMeasured )
2535 {
2636 if ( oboeBridge != null ) return ;
2737
2838 try
2939 {
30- oboeBridge = OboeAudioBridge . Create ( ) ;
40+ var bridge = OboeAudioBridge . Create ( ) ;
3141
32- if ( oboeBridge != null )
42+ if ( bridge != null )
3343 {
34- bool started = oboeBridge . Start ( ) ;
44+ oboeBridge = bridge ;
45+ bool started = bridge . Start ( ) ;
3546
3647 if ( started )
3748 {
38- logOboeInfo ( ) ;
49+ logOboeInfo ( bridge ) ;
3950
40- // Latency is measured asynchronously by the audio callback.
41- // Schedule a check after a short warm-up period to get a stable reading.
4251 scheduler . AddDelayed ( ( ) =>
4352 {
44- if ( oboeBridge == null ) return ;
53+ if ( oboeBridge is not OboeAudioBridge b ) return ;
4554
46- double latency = oboeBridge . GetOutputLatencyMs ( ) ;
55+ double latency = b . GetOutputLatencyMs ( ) ;
4756 Debug . WriteLine ( $ "[osu!] Oboe measured latency after warm-up: { latency : F1} ms") ;
4857
4958 if ( latency > 0 )
@@ -62,86 +71,110 @@ public void StartOboeBridge(Scheduler scheduler, Action<double> onLatencyMeasure
6271 }
6372 }
6473
74+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
6575 public void StopOboeBridge ( )
6676 {
67- oboeBridge ? . Dispose ( ) ;
77+ ( oboeBridge as OboeAudioBridge ) ? . Dispose ( ) ;
6878 oboeBridge = null ;
6979 Debug . WriteLine ( "[osu!] Oboe bridge stopped by user setting" ) ;
7080 }
7181
82+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
83+ public double GetMeasuredAudioLatencyMs ( )
84+ {
85+ return ( oboeBridge as OboeAudioBridge ) ? . GetOutputLatencyMs ( ) ?? - 1 ;
86+ }
87+
88+ // ── Vulkan ─────────────────────────────────────────────────────────
89+
90+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
7291 public void StartVulkanProbe ( )
7392 {
7493 if ( vulkanProbe != null ) return ;
7594
7695 try
7796 {
78- vulkanProbe = VulkanProbe . Create ( ) ;
97+ var probe = VulkanProbe . Create ( ) ;
7998
80- if ( vulkanProbe != null )
81- logVulkanInfo ( ) ;
99+ if ( probe != null )
100+ {
101+ vulkanProbe = probe ;
102+ logVulkanInfo ( probe ) ;
103+ }
82104 }
83105 catch ( Exception e )
84106 {
85107 Debug . WriteLine ( $ "[osu!] Vulkan probe init failed: { e . Message } ") ;
86108 }
87109 }
88110
111+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
89112 public void StopVulkanProbe ( )
90113 {
91- vulkanProbe ? . Dispose ( ) ;
114+ ( vulkanProbe as VulkanProbe ) ? . Dispose ( ) ;
92115 vulkanProbe = null ;
93116 Debug . WriteLine ( "[osu!] Vulkan probe stopped by user setting" ) ;
94117 }
95118
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- }
119+ // ── Logging ────────────────────────────────────────────────────────
104120
105- private void logVulkanInfo ( )
121+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
122+ private static void logVulkanInfo ( VulkanProbe probe )
106123 {
107- if ( vulkanProbe == null ) return ;
108-
109- int ver = vulkanProbe . ApiVersion ;
124+ int ver = probe . ApiVersion ;
110125 int major = ( ver >> 22 ) & 0x3FF ;
111126 int minor = ( ver >> 12 ) & 0x3FF ;
112127 int patch = ver & 0xFFF ;
113128
114- Debug . WriteLine ( $ "[osu!] Vulkan GPU: available={ vulkanProbe . IsAvailable } , "
129+ Debug . WriteLine ( $ "[osu!] Vulkan GPU: available={ probe . IsAvailable } , "
115130 + $ "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 } ") ;
131+ + $ "swapchain={ probe . SupportsSwapchain } , "
132+ + $ "mailbox={ probe . SupportsMailboxPresentMode } , "
133+ + $ "VRAM={ probe . DeviceLocalMemoryMB } MB, "
134+ + $ "queueFamilies={ probe . QueueFamilyCount } , "
135+ + $ "dedicatedCompute={ probe . HasDedicatedComputeQueue } , "
136+ + $ "dedicatedTransfer={ probe . HasDedicatedTransferQueue } ") ;
122137 }
123138
124- private void logOboeInfo ( )
139+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
140+ private static void logOboeInfo ( OboeAudioBridge bridge )
125141 {
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") ;
142+ Debug . WriteLine ( $ "[osu!] Oboe audio: active={ bridge . IsActive } , "
143+ + $ "api={ ( bridge . IsAAudio ? "AAudio" : "OpenSLES" ) } , "
144+ + $ "sampleRate={ bridge . SampleRate } Hz, "
145+ + $ "burst={ bridge . FramesPerBurst } frames, "
146+ + $ "bufferSize={ bridge . BufferSizeInFrames } frames") ;
133147 }
134148
149+ // ── Cleanup ────────────────────────────────────────────────────────
150+
151+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
135152 public void Dispose ( )
136153 {
137154 if ( disposed ) return ;
138155
139156 disposed = true ;
140157
141- oboeBridge ? . Dispose ( ) ;
158+ try
159+ {
160+ ( oboeBridge as OboeAudioBridge ) ? . Dispose ( ) ;
161+ }
162+ catch ( Exception e )
163+ {
164+ Debug . WriteLine ( $ "[osu!] Oboe dispose failed: { e . Message } ") ;
165+ }
166+
142167 oboeBridge = null ;
143168
144- vulkanProbe ? . Dispose ( ) ;
169+ try
170+ {
171+ ( vulkanProbe as VulkanProbe ) ? . Dispose ( ) ;
172+ }
173+ catch ( Exception e )
174+ {
175+ Debug . WriteLine ( $ "[osu!] Vulkan dispose failed: { e . Message } ") ;
176+ }
177+
145178 vulkanProbe = null ;
146179 }
147180 }
0 commit comments