Skip to content

Commit 85ae474

Browse files
Copilotwinnerspiros
andcommitted
Upgrade to NDK r29, Vulkan 1.3, Oboe 1.10.0
Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/89b53171-bb76-44d0-9986-affb5059fb3c Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
1 parent 5cd1f3c commit 85ae474

9 files changed

Lines changed: 209 additions & 20 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ jobs:
3535
- name: Ensure Android NDK and CMake are available
3636
run: |
3737
# The ubuntu-latest runner has $ANDROID_HOME pre-installed.
38-
# Install a known NDK + CMake version if not already present.
38+
# Install latest stable NDK (r29) + CMake.
3939
yes | sdkmanager --licenses > /dev/null 2>&1 || true
40-
sdkmanager --install "ndk;27.0.12077973" "cmake;3.22.1" > /dev/null 2>&1
40+
sdkmanager --install "ndk;29.0.14206865" "cmake;3.22.1" > /dev/null 2>&1
4141
4242
- name: Build native library (libosu_native.so)
4343
run: |
44-
NDK_HOME="$ANDROID_HOME/ndk/27.0.12077973"
44+
NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
4545
CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake"
4646
4747
for ABI in arm64-v8a armeabi-v7a x86; do

osu.Android/AndroidNativeBridgeManager.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,18 @@ private static void logVulkanInfo(VulkanProbe probe)
133133
+ $"VRAM={probe.DeviceLocalMemoryMB}MB, "
134134
+ $"queueFamilies={probe.QueueFamilyCount}, "
135135
+ $"dedicatedCompute={probe.HasDedicatedComputeQueue}, "
136-
+ $"dedicatedTransfer={probe.HasDedicatedTransferQueue}");
136+
+ $"dedicatedTransfer={probe.HasDedicatedTransferQueue}, "
137+
+ $"vk1.3={probe.MeetsVulkan13}, "
138+
+ $"dynamicRendering={probe.SupportsDynamicRendering}, "
139+
+ $"synchronization2={probe.SupportsSynchronization2}");
137140
}
138141

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

osu.Android/Native/CMakeLists.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ set(CMAKE_CXX_STANDARD 17)
77
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -flto -fvisibility=hidden -DNDEBUG")
88
set(CMAKE_C_FLAGS_RELEASE "-O2 -flto -fvisibility=hidden -DNDEBUG")
99

10+
# Disable Oboe's flowgraph module — we don't use any audio processing/conversion
11+
# features (our bridge outputs silence for latency measurement only).
12+
# This reduces the Oboe portion of the binary by ~50%.
13+
set(OBOE_ENABLE_FLOWGRAPH OFF CACHE BOOL "Disable Oboe flowgraph to reduce binary size")
14+
1015
# Try pre-installed Oboe first (e.g. via Android NDK prefab or local install).
1116
# If not found, download and build from source for CI/hermetic builds.
1217
find_package(oboe QUIET CONFIG)
@@ -16,8 +21,8 @@ if(oboe_FOUND)
1621
else()
1722
include(FetchContent)
1823
FetchContent_Declare(oboe
19-
URL https://github.com/google/oboe/archive/refs/tags/1.9.0.tar.gz
20-
URL_HASH SHA256=e030276d25b8bdfaeb04f66646821b1ba4a2b6a580a7e84fb144a478eaecd663
24+
URL https://github.com/google/oboe/archive/refs/tags/1.10.0.tar.gz
25+
URL_HASH SHA256=0e4245f8860c4287040a5d76501c588490bcc9cb57614c486c0c201a5dde3e9f
2126
)
2227
FetchContent_MakeAvailable(oboe)
2328
set(OBOE_LIB oboe)

osu.Android/Native/OboeAudioBridge.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,28 @@ public bool IsAAudio
227227
}
228228
}
229229

230+
/// <summary>
231+
/// Whether the stream is using the hardware MMAP path (lowest possible latency).
232+
/// MMAP provides direct memory-mapped access to audio hardware buffers,
233+
/// bypassing the normal kernel copy path.
234+
/// </summary>
235+
public bool IsMMap
236+
{
237+
get
238+
{
239+
if (disposed || nativePtr == IntPtr.Zero) return false;
240+
241+
try
242+
{
243+
return nOboeIsMMap(nativePtr) != 0;
244+
}
245+
catch
246+
{
247+
return false;
248+
}
249+
}
250+
}
251+
230252
public void Dispose()
231253
{
232254
if (disposed) return;
@@ -284,5 +306,8 @@ public void Dispose()
284306

285307
[DllImport(lib_name)]
286308
private static extern byte nOboeIsAAudio(IntPtr ptr);
309+
310+
[DllImport(lib_name)]
311+
private static extern byte nOboeIsMMap(IntPtr ptr);
287312
}
288313
}

osu.Android/Native/VulkanProbe.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,69 @@ public bool SupportsMailboxPresentMode
233233
}
234234
}
235235

236+
/// <summary>
237+
/// Whether the device reports Vulkan 1.3+ API version.
238+
/// </summary>
239+
public bool MeetsVulkan13
240+
{
241+
get
242+
{
243+
if (disposed || nativePtr == IntPtr.Zero) return false;
244+
245+
try
246+
{
247+
return nVulkanMeetsVulkan13(nativePtr) != 0;
248+
}
249+
catch
250+
{
251+
return false;
252+
}
253+
}
254+
}
255+
256+
/// <summary>
257+
/// Whether the device supports VkPhysicalDeviceVulkan13Features::dynamicRendering.
258+
/// Dynamic rendering eliminates VkRenderPass/VkFramebuffer boilerplate for simpler,
259+
/// more flexible rendering.
260+
/// </summary>
261+
public bool SupportsDynamicRendering
262+
{
263+
get
264+
{
265+
if (disposed || nativePtr == IntPtr.Zero) return false;
266+
267+
try
268+
{
269+
return nVulkanSupportsDynamicRendering(nativePtr) != 0;
270+
}
271+
catch
272+
{
273+
return false;
274+
}
275+
}
276+
}
277+
278+
/// <summary>
279+
/// Whether the device supports VkPhysicalDeviceVulkan13Features::synchronization2.
280+
/// Provides a cleaner, less error-prone GPU synchronization model.
281+
/// </summary>
282+
public bool SupportsSynchronization2
283+
{
284+
get
285+
{
286+
if (disposed || nativePtr == IntPtr.Zero) return false;
287+
288+
try
289+
{
290+
return nVulkanSupportsSynchronization2(nativePtr) != 0;
291+
}
292+
catch
293+
{
294+
return false;
295+
}
296+
}
297+
}
298+
236299
public void Dispose()
237300
{
238301
if (disposed) return;
@@ -290,5 +353,14 @@ public void Dispose()
290353

291354
[DllImport(lib_name)]
292355
private static extern byte nVulkanSupportsMailboxPresentMode(IntPtr ptr);
356+
357+
[DllImport(lib_name)]
358+
private static extern byte nVulkanMeetsVulkan13(IntPtr ptr);
359+
360+
[DllImport(lib_name)]
361+
private static extern byte nVulkanSupportsDynamicRendering(IntPtr ptr);
362+
363+
[DllImport(lib_name)]
364+
private static extern byte nVulkanSupportsSynchronization2(IntPtr ptr);
293365
}
294366
}

osu.Android/Native/oboe_bridge.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENCE file in the repository root for full licence text.
33

44
#include "oboe_bridge.h"
5+
#include <oboe/OboeExtensions.h>
56
#include <android/log.h>
67
#include <cstdint>
78
#include <cstring>
@@ -62,13 +63,14 @@ bool OboeBridge::open() {
6263
optimiseBufferSize();
6364

6465
LOGI("Oboe stream opened: api=%s, sampleRate=%d, framesPerBurst=%d, "
65-
"bufferSize=%d, bufferCapacity=%d, sharingMode=%s",
66+
"bufferSize=%d, bufferCapacity=%d, sharingMode=%s, mmap=%s",
6667
stream_->getAudioApi() == oboe::AudioApi::AAudio ? "AAudio" : "OpenSLES",
6768
stream_->getSampleRate(),
6869
stream_->getFramesPerBurst(),
6970
stream_->getBufferSizeInFrames(),
7071
stream_->getBufferCapacityInFrames(),
71-
stream_->getSharingMode() == oboe::SharingMode::Exclusive ? "Exclusive" : "Shared");
72+
stream_->getSharingMode() == oboe::SharingMode::Exclusive ? "Exclusive" : "Shared",
73+
oboe::OboeExtensions::isMMapUsed(stream_.get()) ? "yes" : "no");
7274

7375
return true;
7476
}
@@ -152,6 +154,11 @@ bool OboeBridge::isAAudio() const {
152154
return stream_ && stream_->getAudioApi() == oboe::AudioApi::AAudio;
153155
}
154156

157+
bool OboeBridge::isMMap() const {
158+
std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(streamLock_));
159+
return stream_ && oboe::OboeExtensions::isMMapUsed(stream_.get());
160+
}
161+
155162
oboe::DataCallbackResult OboeBridge::onAudioReady(
156163
oboe::AudioStream* stream, void* audioData, int32_t numFrames) {
157164

@@ -297,4 +304,9 @@ OSU_EXPORT unsigned char nOboeIsAAudio(intptr_t ptr) {
297304
return (bridge && bridge->isAAudio()) ? 1 : 0;
298305
}
299306

307+
OSU_EXPORT unsigned char nOboeIsMMap(intptr_t ptr) {
308+
auto* bridge = reinterpret_cast<OboeBridge*>(ptr);
309+
return (bridge && bridge->isMMap()) ? 1 : 0;
310+
}
311+
300312
} // extern "C"

osu.Android/Native/oboe_bridge.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class OboeBridge : public oboe::AudioStreamCallback {
4040
/// Returns true if the stream is using AAudio (vs OpenSL ES fallback).
4141
bool isAAudio() const;
4242

43+
/// Returns true if the stream is using the hardware MMAP path (lowest possible latency).
44+
/// MMAP provides direct memory-mapped access to audio hardware buffers.
45+
bool isMMap() const;
46+
4347
// oboe::AudioStreamCallback
4448
oboe::DataCallbackResult onAudioReady(
4549
oboe::AudioStream* stream, void* audioData, int32_t numFrames) override;

osu.Android/Native/vulkan_bridge.cpp

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ VulkanProbe::VulkanProbe() {
2121

2222
if (available_) {
2323
LOGI("Vulkan available: %s (API %u.%u.%u, driver %u, VRAM %u MB, "
24-
"queues %u, dedicatedCompute=%d, dedicatedTransfer=%d, swapchain=%d, mailbox=%d)",
24+
"queues %u, dedicatedCompute=%d, dedicatedTransfer=%d, swapchain=%d, mailbox=%d, "
25+
"vk1.3=%d, dynamicRendering=%d, synchronization2=%d)",
2526
deviceInfo_.deviceName.c_str(),
2627
VK_VERSION_MAJOR(deviceInfo_.apiVersion),
2728
VK_VERSION_MINOR(deviceInfo_.apiVersion),
@@ -32,7 +33,10 @@ VulkanProbe::VulkanProbe() {
3233
deviceInfo_.hasDedicatedComputeQueue ? 1 : 0,
3334
deviceInfo_.hasDedicatedTransferQueue ? 1 : 0,
3435
deviceInfo_.supportsSwapchain ? 1 : 0,
35-
deviceInfo_.supportsMailboxPresentMode ? 1 : 0);
36+
deviceInfo_.supportsMailboxPresentMode ? 1 : 0,
37+
deviceInfo_.meetsVulkan13 ? 1 : 0,
38+
deviceInfo_.supportsDynamicRendering ? 1 : 0,
39+
deviceInfo_.supportsSynchronization2 ? 1 : 0);
3640
} else {
3741
LOGI("Vulkan not available on this device");
3842
}
@@ -49,7 +53,9 @@ bool VulkanProbe::createInstance() {
4953
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
5054
appInfo.pEngineName = "osu-framework";
5155
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
52-
appInfo.apiVersion = VK_API_VERSION_1_0;
56+
// Request Vulkan 1.3 to enable full feature queries (dynamic rendering,
57+
// synchronization2). Falls back to 1.0 on older drivers.
58+
appInfo.apiVersion = VK_API_VERSION_1_3;
5359

5460
VkInstanceCreateInfo createInfo{};
5561
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
@@ -59,6 +65,13 @@ bool VulkanProbe::createInstance() {
5965

6066
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance_);
6167

68+
if (result == VK_ERROR_INCOMPATIBLE_DRIVER) {
69+
// Vulkan 1.0-only driver; fall back.
70+
LOGI("Vulkan 1.3 instance not supported, falling back to 1.0");
71+
appInfo.apiVersion = VK_API_VERSION_1_0;
72+
result = vkCreateInstance(&createInfo, nullptr, &instance_);
73+
}
74+
6275
if (result != VK_SUCCESS) {
6376
LOGE("vkCreateInstance failed: %d", result);
6477
return false;
@@ -130,6 +143,7 @@ bool VulkanProbe::queryDevice() {
130143
queryMemory(selected);
131144
queryQueueFamilies(selected);
132145
queryMailboxSupport(selected);
146+
queryVulkan13Features(selected);
133147

134148
return true;
135149
}
@@ -203,6 +217,33 @@ void VulkanProbe::queryMailboxSupport(VkPhysicalDevice device) {
203217
}
204218
}
205219

220+
void VulkanProbe::queryVulkan13Features(VkPhysicalDevice device) {
221+
// Check if the device reports Vulkan 1.3+.
222+
uint32_t major = VK_VERSION_MAJOR(deviceInfo_.apiVersion);
223+
uint32_t minor = VK_VERSION_MINOR(deviceInfo_.apiVersion);
224+
225+
if (major < 1 || (major == 1 && minor < 3)) {
226+
LOGI("Device Vulkan API %u.%u < 1.3, skipping 1.3 feature query", major, minor);
227+
return;
228+
}
229+
230+
deviceInfo_.meetsVulkan13 = true;
231+
232+
// vkGetPhysicalDeviceFeatures2 is available since Vulkan 1.1, and the device
233+
// reports 1.3+, so this is safe.
234+
VkPhysicalDeviceVulkan13Features features13{};
235+
features13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES;
236+
237+
VkPhysicalDeviceFeatures2 features2{};
238+
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
239+
features2.pNext = &features13;
240+
241+
vkGetPhysicalDeviceFeatures2(device, &features2);
242+
243+
deviceInfo_.supportsDynamicRendering = features13.dynamicRendering == VK_TRUE;
244+
deviceInfo_.supportsSynchronization2 = features13.synchronization2 == VK_TRUE;
245+
}
246+
206247
void VulkanProbe::cleanup() {
207248
if (instance_ != VK_NULL_HANDLE) {
208249
vkDestroyInstance(instance_, nullptr);
@@ -272,4 +313,19 @@ OSU_EXPORT unsigned char nVulkanSupportsMailboxPresentMode(intptr_t ptr) {
272313
return (probe && probe->getDeviceInfo().supportsMailboxPresentMode) ? 1 : 0;
273314
}
274315

316+
OSU_EXPORT unsigned char nVulkanMeetsVulkan13(intptr_t ptr) {
317+
auto* probe = reinterpret_cast<VulkanProbe*>(ptr);
318+
return (probe && probe->getDeviceInfo().meetsVulkan13) ? 1 : 0;
319+
}
320+
321+
OSU_EXPORT unsigned char nVulkanSupportsDynamicRendering(intptr_t ptr) {
322+
auto* probe = reinterpret_cast<VulkanProbe*>(ptr);
323+
return (probe && probe->getDeviceInfo().supportsDynamicRendering) ? 1 : 0;
324+
}
325+
326+
OSU_EXPORT unsigned char nVulkanSupportsSynchronization2(intptr_t ptr) {
327+
auto* probe = reinterpret_cast<VulkanProbe*>(ptr);
328+
return (probe && probe->getDeviceInfo().supportsSynchronization2) ? 1 : 0;
329+
}
330+
275331
} // extern "C"

0 commit comments

Comments
 (0)