diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e73228573db6..9246f94c68d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: "$CMAKE_BIN" -B "build-native/$ABI" -S osu.Android/Native \ -DCMAKE_TOOLCHAIN_FILE="$NDK_HOME/build/cmake/android.toolchain.cmake" \ -DANDROID_ABI="$ABI" \ - -DANDROID_PLATFORM=android-30 \ + -DANDROID_PLATFORM=android-31 \ -DCMAKE_BUILD_TYPE=Release "$CMAKE_BIN" --build "build-native/$ABI" --config Release -j "$(nproc)" mkdir -p "osu.Android/libs/$ABI" diff --git a/osu.Android.props b/osu.Android.props index f1383d5cc261..b12b28395ad7 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -1,6 +1,6 @@  - 30.0 + 31.0 android-x86;android-arm;android-arm64 apk CJK;Mideast;Rare;West;Other; diff --git a/osu.Android/AndroidManifest.xml b/osu.Android/AndroidManifest.xml index fa98585b4342..46f875170743 100644 --- a/osu.Android/AndroidManifest.xml +++ b/osu.Android/AndroidManifest.xml @@ -1,6 +1,6 @@  - + - /// Probes Vulkan GPU capability on the current Android device. - /// Used to report hardware information and determine optimal rendering strategy - /// for low-latency gameplay. - /// public sealed class VulkanProbe : IDisposable { private const string lib_name = "osu_native"; - - private IntPtr nativePtr; - private volatile bool disposed; - private static readonly bool native_loaded; + private IntPtr nativePtr; + private bool disposed; static VulkanProbe() { - try - { - // Use DllImportSearchPath.ApplicationDirectory to avoid searching system paths - // which can crash on some Samsung devices with aggressive security policies. - native_loaded = NativeLibrary.TryLoad( - lib_name, - typeof(VulkanProbe).Assembly, - DllImportSearchPath.ApplicationDirectory, - out _); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Failed to probe native library for Vulkan: {e.Message}"); - native_loaded = false; - } - - if (!native_loaded) - Debug.WriteLine("[osu!] Native library not found, Vulkan probe unavailable"); + try { native_loaded = NativeLibrary.TryLoad(lib_name, typeof(VulkanProbe).Assembly, DllImportSearchPath.ApplicationDirectory, out _); } + catch { native_loaded = false; } } - /// - /// Creates a Vulkan probe. Returns null if native library is unavailable. - /// public static VulkanProbe? Create() { - if (!native_loaded) - return null; - - try - { - IntPtr ptr = nVulkanProbeCreate(); - - if (ptr == IntPtr.Zero) - { - Debug.WriteLine("[osu!] Vulkan probe creation failed"); - return null; - } - - return new VulkanProbe(ptr); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Vulkan probe failed: {e.Message}"); - return null; - } - } - - private VulkanProbe(IntPtr ptr) - { - nativePtr = ptr; - } - - /// - /// Whether Vulkan is available on this device. - /// - public bool IsAvailable - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanIsAvailable(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// The Vulkan API version supported by the device, encoded as per Vulkan spec. - /// - public int ApiVersion - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return 0; - - try - { - return nVulkanGetApiVersion(nativePtr); - } - catch - { - return 0; - } - } - } - - /// - /// Whether the device supports VK_KHR_swapchain (required for rendering). - /// - public bool SupportsSwapchain - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanSupportsSwapchain(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Total device-local GPU memory in megabytes. - /// - public int DeviceLocalMemoryMB - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return 0; - - try - { - return nVulkanGetDeviceLocalMemoryMB(nativePtr); - } - catch - { - return 0; - } - } - } - - /// - /// Number of queue families available on the device. - /// - public int QueueFamilyCount - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return 0; - - try - { - return nVulkanGetQueueFamilyCount(nativePtr); - } - catch - { - return 0; - } - } - } - - /// - /// Whether the device has a dedicated compute queue (separate from graphics). - /// Enables async compute for better frame pacing. - /// - public bool HasDedicatedComputeQueue - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanHasDedicatedComputeQueue(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Whether the device has a dedicated transfer queue. - /// Enables async upload for reduced frame stalls. - /// - public bool HasDedicatedTransferQueue - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanHasDedicatedTransferQueue(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Whether the device likely supports MAILBOX present mode for low-latency triple-buffered rendering. - /// Detected via the VK_GOOGLE_display_timing extension, which is present on Android GPUs - /// (Adreno, Mali) that also expose MAILBOX present mode. - /// - public bool SupportsMailboxPresentMode - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanSupportsMailboxPresentMode(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Whether the device reports Vulkan 1.3+ API version. - /// - public bool MeetsVulkan13 - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanMeetsVulkan13(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Whether the device supports VkPhysicalDeviceVulkan13Features::dynamicRendering. - /// Dynamic rendering eliminates VkRenderPass/VkFramebuffer boilerplate for simpler, - /// more flexible rendering. - /// - public bool SupportsDynamicRendering - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanSupportsDynamicRendering(nativePtr) != 0; - } - catch - { - return false; - } - } - } - - /// - /// Whether the device supports VkPhysicalDeviceVulkan13Features::synchronization2. - /// Provides a cleaner, less error-prone GPU synchronization model. - /// - public bool SupportsSynchronization2 - { - get - { - if (disposed || nativePtr == IntPtr.Zero) return false; - - try - { - return nVulkanSupportsSynchronization2(nativePtr) != 0; - } - catch - { - return false; - } - } - } + if (!native_loaded) return null; + try { IntPtr ptr = nVulkanProbeCreate(); return ptr == IntPtr.Zero ? null : new VulkanProbe(ptr); } + catch { return null; } + } + + private VulkanProbe(IntPtr ptr) => nativePtr = ptr; + + public bool IsAvailable => !disposed && nativePtr != IntPtr.Zero && nVulkanIsAvailable(nativePtr) != 0; + public int ApiVersion => !disposed && nativePtr != IntPtr.Zero ? nVulkanGetApiVersion(nativePtr) : 0; + public bool SupportsSwapchain => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsSwapchain(nativePtr) != 0; + public int DeviceLocalMemoryMB => !disposed && nativePtr != IntPtr.Zero ? nVulkanGetDeviceLocalMemoryMB(nativePtr) : 0; + public int QueueFamilyCount => !disposed && nativePtr != IntPtr.Zero ? nVulkanGetQueueFamilyCount(nativePtr) : 0; + public bool HasDedicatedComputeQueue => !disposed && nativePtr != IntPtr.Zero && nVulkanHasDedicatedComputeQueue(nativePtr) != 0; + public bool HasDedicatedTransferQueue => !disposed && nativePtr != IntPtr.Zero && nVulkanHasDedicatedTransferQueue(nativePtr) != 0; + public bool SupportsMailboxPresentMode => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsMailboxPresentMode(nativePtr) != 0; + public bool MeetsVulkan13 => !disposed && nativePtr != IntPtr.Zero && nVulkanMeetsVulkan13(nativePtr) != 0; + public bool SupportsDynamicRendering => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsDynamicRendering(nativePtr) != 0; + public bool SupportsSynchronization2 => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsSynchronization2(nativePtr) != 0; + public bool SupportsPresentId => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsPresentId(nativePtr) != 0; + public bool SupportsPresentWait => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsPresentWait(nativePtr) != 0; + public bool SupportsGraphicsPipelineLibrary => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsGraphicsPipelineLibrary(nativePtr) != 0; + public bool SupportsShaderObject => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsShaderObject(nativePtr) != 0; + public bool SupportsGlobalPriority => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsGlobalPriority(nativePtr) != 0; + public bool SupportsMemoryBudget => !disposed && nativePtr != IntPtr.Zero && nVulkanSupportsMemoryBudget(nativePtr) != 0; public void Dispose() { if (disposed) return; - disposed = true; - - if (nativePtr != IntPtr.Zero) - { - try - { - nVulkanProbeDestroy(nativePtr); - } - catch (Exception e) - { - Debug.WriteLine($"[osu!] Vulkan probe dispose failed: {e.Message}"); - } - - nativePtr = IntPtr.Zero; - } - + if (nativePtr != IntPtr.Zero) { try { nVulkanProbeDestroy(nativePtr); } catch { } nativePtr = IntPtr.Zero; } GC.SuppressFinalize(this); } - ~VulkanProbe() - { - Dispose(); - } - - [DllImport(lib_name)] - private static extern IntPtr nVulkanProbeCreate(); - - [DllImport(lib_name)] - private static extern void nVulkanProbeDestroy(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanIsAvailable(IntPtr ptr); - - [DllImport(lib_name)] - private static extern int nVulkanGetApiVersion(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanSupportsSwapchain(IntPtr ptr); - - [DllImport(lib_name)] - private static extern int nVulkanGetDeviceLocalMemoryMB(IntPtr ptr); - - [DllImport(lib_name)] - private static extern int nVulkanGetQueueFamilyCount(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanHasDedicatedComputeQueue(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanHasDedicatedTransferQueue(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanSupportsMailboxPresentMode(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanMeetsVulkan13(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanSupportsDynamicRendering(IntPtr ptr); - - [DllImport(lib_name)] - private static extern byte nVulkanSupportsSynchronization2(IntPtr ptr); + ~VulkanProbe() => Dispose(); + + [DllImport(lib_name)] private static extern IntPtr nVulkanProbeCreate(); + [DllImport(lib_name)] private static extern void nVulkanProbeDestroy(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanIsAvailable(IntPtr ptr); + [DllImport(lib_name)] private static extern int nVulkanGetApiVersion(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsSwapchain(IntPtr ptr); + [DllImport(lib_name)] private static extern int nVulkanGetDeviceLocalMemoryMB(IntPtr ptr); + [DllImport(lib_name)] private static extern int nVulkanGetQueueFamilyCount(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanHasDedicatedComputeQueue(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanHasDedicatedTransferQueue(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsMailboxPresentMode(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanMeetsVulkan13(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsDynamicRendering(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsSynchronization2(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsPresentId(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsPresentWait(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsGraphicsPipelineLibrary(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsShaderObject(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsGlobalPriority(IntPtr ptr); + [DllImport(lib_name)] private static extern byte nVulkanSupportsMemoryBudget(IntPtr ptr); } } diff --git a/osu.Android/Native/oboe_bridge.cpp b/osu.Android/Native/oboe_bridge.cpp index bb39a31af64b..15a960f4a6f1 100644 --- a/osu.Android/Native/oboe_bridge.cpp +++ b/osu.Android/Native/oboe_bridge.cpp @@ -193,7 +193,7 @@ oboe::DataCallbackResult OboeBridge::onAudioReady( // Reporting actual work duration helps ADPF (Android Dynamic Performance Framework) // adjust CPU frequency precisely to handle the audio load without skipping. int64_t endTime = oboe::AudioClock::getNanoseconds(); - stream->reportActualWorkDuration(endTime - startTime); + if (stream->isPerformanceHintEnabled()) { stream->reportActualWorkDuration(endTime - startTime); } uint32_t count = callbackCount_.fetch_add(1, std::memory_order_relaxed); diff --git a/osu.Android/Native/vulkan_bridge.cpp b/osu.Android/Native/vulkan_bridge.cpp index a5fa5a99a35f..41abc73d4c13 100644 --- a/osu.Android/Native/vulkan_bridge.cpp +++ b/osu.Android/Native/vulkan_bridge.cpp @@ -21,8 +21,7 @@ VulkanProbe::VulkanProbe() { if (available_) { LOGI("Vulkan available: %s (API %u.%u.%u, driver %u, VRAM %u MB, " - "queues %u, dedicatedCompute=%d, dedicatedTransfer=%d, swapchain=%d, mailbox=%d, " - "vk1.3=%d, dynamicRendering=%d, synchronization2=%d)", + "queues %u, mailbox=%d, vk1.3=%d, sync2=%d, presentWait=%d, gpl=%d, shaderObj=%d, priority=%d)", deviceInfo_.deviceName.c_str(), VK_VERSION_MAJOR(deviceInfo_.apiVersion), VK_VERSION_MINOR(deviceInfo_.apiVersion), @@ -30,13 +29,13 @@ VulkanProbe::VulkanProbe() { deviceInfo_.driverVersion, deviceInfo_.deviceLocalMemoryMB, deviceInfo_.queueFamilyCount, - deviceInfo_.hasDedicatedComputeQueue ? 1 : 0, - deviceInfo_.hasDedicatedTransferQueue ? 1 : 0, - deviceInfo_.supportsSwapchain ? 1 : 0, deviceInfo_.supportsMailboxPresentMode ? 1 : 0, deviceInfo_.meetsVulkan13 ? 1 : 0, - deviceInfo_.supportsDynamicRendering ? 1 : 0, - deviceInfo_.supportsSynchronization2 ? 1 : 0); + deviceInfo_.supportsSynchronization2 ? 1 : 0, + deviceInfo_.supportsPresentWait ? 1 : 0, + deviceInfo_.supportsGraphicsPipelineLibrary ? 1 : 0, + deviceInfo_.supportsShaderObject ? 1 : 0, + deviceInfo_.supportsGlobalPriority ? 1 : 0); } else { LOGI("Vulkan not available on this device"); } @@ -51,58 +50,37 @@ bool VulkanProbe::createInstance() { appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pApplicationName = "osu!"; appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.pEngineName = "osu-framework"; + appInfo.pEngineName = "No Engine"; appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); - // Request Vulkan 1.3 to enable full feature queries (dynamic rendering, - // synchronization2). Falls back to 1.0 on older drivers. appInfo.apiVersion = VK_API_VERSION_1_3; VkInstanceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; - createInfo.enabledLayerCount = 0; - createInfo.enabledExtensionCount = 0; VkResult result = vkCreateInstance(&createInfo, nullptr, &instance_); if (result == VK_ERROR_INCOMPATIBLE_DRIVER) { - // Vulkan 1.0-only driver; fall back. - LOGI("Vulkan 1.3 instance not supported, falling back to 1.0"); appInfo.apiVersion = VK_API_VERSION_1_0; result = vkCreateInstance(&createInfo, nullptr, &instance_); } - if (result != VK_SUCCESS) { - LOGE("vkCreateInstance failed: %d", result); - return false; - } - + if (result != VK_SUCCESS) return false; return true; } bool VulkanProbe::queryDevice() { uint32_t deviceCount = 0; vkEnumeratePhysicalDevices(instance_, &deviceCount, nullptr); - - if (deviceCount == 0) { - LOGI("No Vulkan physical devices found"); - return false; - } + if (deviceCount == 0) return false; std::vector devices(deviceCount); + if (vkEnumeratePhysicalDevices(instance_, &deviceCount, devices.data()) != VK_SUCCESS) return false; - if (vkEnumeratePhysicalDevices(instance_, &deviceCount, devices.data()) != VK_SUCCESS || deviceCount == 0) { - LOGE("Failed to enumerate Vulkan physical devices"); - return false; - } - - // Pick the first discrete GPU, or fall back to the first device. VkPhysicalDevice selected = devices[0]; - for (const auto& dev : devices) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(dev, &props); - if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { selected = dev; break; @@ -111,39 +89,16 @@ bool VulkanProbe::queryDevice() { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(selected, &props); - deviceInfo_.deviceName = props.deviceName; deviceInfo_.apiVersion = props.apiVersion; deviceInfo_.driverVersion = props.driverVersion; deviceInfo_.vendorId = props.vendorID; - // Check for swapchain extension support. - uint32_t extCount = 0; - - if (vkEnumerateDeviceExtensionProperties(selected, nullptr, &extCount, nullptr) != VK_SUCCESS || extCount == 0) { - deviceInfo_.supportsSwapchain = false; - } else { - std::vector extensions(extCount); - - if (vkEnumerateDeviceExtensionProperties(selected, nullptr, &extCount, extensions.data()) != VK_SUCCESS) { - deviceInfo_.supportsSwapchain = false; - } else { - deviceInfo_.supportsSwapchain = false; - - for (const auto& ext : extensions) { - if (strcmp(ext.extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME) == 0) { - deviceInfo_.supportsSwapchain = true; - break; - } - } - } - } - - // Query additional performance-relevant capabilities. queryMemory(selected); queryQueueFamilies(selected); queryMailboxSupport(selected); queryVulkan13Features(selected); + queryModernExtensions(selected); return true; } @@ -151,97 +106,70 @@ bool VulkanProbe::queryDevice() { void VulkanProbe::queryMemory(VkPhysicalDevice device) { VkPhysicalDeviceMemoryProperties memProps; vkGetPhysicalDeviceMemoryProperties(device, &memProps); - uint64_t deviceLocalBytes = 0; - for (uint32_t i = 0; i < memProps.memoryHeapCount; i++) { if (memProps.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) { deviceLocalBytes += memProps.memoryHeaps[i].size; } } - deviceInfo_.deviceLocalMemoryMB = static_cast(deviceLocalBytes / (1024 * 1024)); } void VulkanProbe::queryQueueFamilies(VkPhysicalDevice device) { - uint32_t queueFamilyCount = 0; - vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); - - deviceInfo_.queueFamilyCount = queueFamilyCount; - deviceInfo_.hasDedicatedComputeQueue = false; - deviceInfo_.hasDedicatedTransferQueue = false; - - if (queueFamilyCount == 0) return; - - std::vector families(queueFamilyCount); - vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, families.data()); - + uint32_t count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr); + deviceInfo_.queueFamilyCount = count; + std::vector families(count); + vkGetPhysicalDeviceQueueFamilyProperties(device, &count, families.data()); for (const auto& family : families) { - // A dedicated compute queue has compute but NOT graphics. - bool hasGraphics = (family.queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0; - bool hasCompute = (family.queueFlags & VK_QUEUE_COMPUTE_BIT) != 0; - bool hasTransfer = (family.queueFlags & VK_QUEUE_TRANSFER_BIT) != 0; - - if (hasCompute && !hasGraphics) { + if ((family.queueFlags & VK_QUEUE_COMPUTE_BIT) && !(family.queueFlags & VK_QUEUE_GRAPHICS_BIT)) deviceInfo_.hasDedicatedComputeQueue = true; - } - - if (hasTransfer && !hasGraphics && !hasCompute) { + if ((family.queueFlags & VK_QUEUE_TRANSFER_BIT) && !(family.queueFlags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT))) deviceInfo_.hasDedicatedTransferQueue = true; - } } } void VulkanProbe::queryMailboxSupport(VkPhysicalDevice device) { - // MAILBOX present mode requires a VkSurface to query definitively, but we detect - // it using VK_GOOGLE_display_timing — a device extension that is present exclusively - // on Android GPUs (Adreno, Mali) that also expose MAILBOX present mode support. - // This gives us a reliable indication without needing an active surface. - deviceInfo_.supportsMailboxPresentMode = false; - - uint32_t extCount = 0; - - if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extCount, nullptr) != VK_SUCCESS || extCount == 0) - return; - - std::vector extensions(extCount); - - if (vkEnumerateDeviceExtensionProperties(device, nullptr, &extCount, extensions.data()) != VK_SUCCESS) - return; - - for (const auto& ext : extensions) { + // mailbox detection via display_timing hint + uint32_t count = 0; + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr); + std::vector exts(count); + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, exts.data()); + for (const auto& ext : exts) { if (strcmp(ext.extensionName, "VK_GOOGLE_display_timing") == 0) { deviceInfo_.supportsMailboxPresentMode = true; - return; + break; } } } -void VulkanProbe::queryVulkan13Features(VkPhysicalDevice device) { - // Check if the device reports Vulkan 1.3+. - uint32_t major = VK_VERSION_MAJOR(deviceInfo_.apiVersion); - uint32_t minor = VK_VERSION_MINOR(deviceInfo_.apiVersion); - - if (major < 1 || (major == 1 && minor < 3)) { - LOGI("Device Vulkan API %u.%u < 1.3, skipping 1.3 feature query", major, minor); - return; +void VulkanProbe::queryModernExtensions(VkPhysicalDevice device) { + uint32_t count = 0; + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr); + std::vector exts(count); + vkEnumerateDeviceExtensionProperties(device, nullptr, &count, exts.data()); + for (const auto& ext : exts) { + if (strcmp(ext.extensionName, VK_KHR_SWAPCHAIN_EXTENSION_NAME) == 0) deviceInfo_.supportsSwapchain = true; + if (strcmp(ext.extensionName, VK_KHR_PRESENT_ID_EXTENSION_NAME) == 0) deviceInfo_.supportsPresentId = true; + if (strcmp(ext.extensionName, VK_KHR_PRESENT_WAIT_EXTENSION_NAME) == 0) deviceInfo_.supportsPresentWait = true; + if (strcmp(ext.extensionName, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME) == 0) deviceInfo_.supportsGraphicsPipelineLibrary = true; + if (strcmp(ext.extensionName, VK_EXT_SHADER_OBJECT_EXTENSION_NAME) == 0) deviceInfo_.supportsShaderObject = true; + if (strcmp(ext.extensionName, VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME) == 0 || strcmp(ext.extensionName, VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME) == 0) deviceInfo_.supportsGlobalPriority = true; + if (strcmp(ext.extensionName, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME) == 0) deviceInfo_.supportsMemoryBudget = true; } +} +void VulkanProbe::queryVulkan13Features(VkPhysicalDevice device) { + if (VK_VERSION_MAJOR(deviceInfo_.apiVersion) < 1 || (VK_VERSION_MAJOR(deviceInfo_.apiVersion) == 1 && VK_VERSION_MINOR(deviceInfo_.apiVersion) < 3)) return; deviceInfo_.meetsVulkan13 = true; - - // vkGetPhysicalDeviceFeatures2 is available since Vulkan 1.1, and the device - // reports 1.3+, so this is safe. - VkPhysicalDeviceVulkan13Features features13{}; - features13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; - - VkPhysicalDeviceFeatures2 features2{}; - features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; - features2.pNext = &features13; - - vkGetPhysicalDeviceFeatures2(device, &features2); - - deviceInfo_.supportsDynamicRendering = features13.dynamicRendering == VK_TRUE; - deviceInfo_.supportsSynchronization2 = features13.synchronization2 == VK_TRUE; + VkPhysicalDeviceVulkan13Features f13{}; + f13.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; + VkPhysicalDeviceFeatures2 f2{}; + f2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; + f2.pNext = &f13; + vkGetPhysicalDeviceFeatures2(device, &f2); + deviceInfo_.supportsDynamicRendering = f13.dynamicRendering == VK_TRUE; + deviceInfo_.supportsSynchronization2 = f13.synchronization2 == VK_TRUE; } void VulkanProbe::cleanup() { @@ -251,81 +179,26 @@ void VulkanProbe::cleanup() { } } -// ============================================================ -// C exports for P/Invoke from .NET -// ============================================================ - -// Use intptr_t for pointer handles so the size matches C# IntPtr on both -// 32-bit (4 bytes) and 64-bit (8 bytes) platforms. The previous use of -// C++ `long` was 4 bytes on 32-bit ARM/x86 but C# `long` is always -// 8 bytes, causing a calling-convention mismatch and crash. - #define OSU_EXPORT __attribute__((visibility("default"))) extern "C" { - -OSU_EXPORT intptr_t nVulkanProbeCreate() { - auto* probe = new (std::nothrow) VulkanProbe(); - return reinterpret_cast(probe); -} - -OSU_EXPORT void nVulkanProbeDestroy(intptr_t ptr) { - if (ptr) delete reinterpret_cast(ptr); -} - -OSU_EXPORT unsigned char nVulkanIsAvailable(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->isAvailable()) ? 1 : 0; -} - -OSU_EXPORT int nVulkanGetApiVersion(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return probe ? static_cast(probe->getDeviceInfo().apiVersion) : 0; -} - -OSU_EXPORT unsigned char nVulkanSupportsSwapchain(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().supportsSwapchain) ? 1 : 0; +OSU_EXPORT intptr_t nVulkanProbeCreate() { return reinterpret_cast(new (std::nothrow) VulkanProbe()); } +OSU_EXPORT void nVulkanProbeDestroy(intptr_t ptr) { if (ptr) delete reinterpret_cast(ptr); } +OSU_EXPORT byte nVulkanIsAvailable(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->isAvailable()) ? 1 : 0; } +OSU_EXPORT int nVulkanGetApiVersion(intptr_t ptr) { return ptr ? (int)reinterpret_cast(ptr)->getDeviceInfo().apiVersion : 0; } +OSU_EXPORT byte nVulkanSupportsSwapchain(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsSwapchain) ? 1 : 0; } +OSU_EXPORT int nVulkanGetDeviceLocalMemoryMB(intptr_t ptr) { return ptr ? (int)reinterpret_cast(ptr)->getDeviceInfo().deviceLocalMemoryMB : 0; } +OSU_EXPORT int nVulkanGetQueueFamilyCount(intptr_t ptr) { return ptr ? (int)reinterpret_cast(ptr)->getDeviceInfo().queueFamilyCount : 0; } +OSU_EXPORT byte nVulkanHasDedicatedComputeQueue(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().hasDedicatedComputeQueue) ? 1 : 0; } +OSU_EXPORT byte nVulkanHasDedicatedTransferQueue(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().hasDedicatedTransferQueue) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsMailboxPresentMode(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsMailboxPresentMode) ? 1 : 0; } +OSU_EXPORT byte nVulkanMeetsVulkan13(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().meetsVulkan13) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsDynamicRendering(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsDynamicRendering) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsSynchronization2(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsSynchronization2) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsPresentId(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsPresentId) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsPresentWait(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsPresentWait) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsGraphicsPipelineLibrary(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsGraphicsPipelineLibrary) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsShaderObject(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsShaderObject) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsGlobalPriority(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsGlobalPriority) ? 1 : 0; } +OSU_EXPORT byte nVulkanSupportsMemoryBudget(intptr_t ptr) { return (ptr && reinterpret_cast(ptr)->getDeviceInfo().supportsMemoryBudget) ? 1 : 0; } } - -OSU_EXPORT int nVulkanGetDeviceLocalMemoryMB(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return probe ? static_cast(probe->getDeviceInfo().deviceLocalMemoryMB) : 0; -} - -OSU_EXPORT int nVulkanGetQueueFamilyCount(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return probe ? static_cast(probe->getDeviceInfo().queueFamilyCount) : 0; -} - -OSU_EXPORT unsigned char nVulkanHasDedicatedComputeQueue(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().hasDedicatedComputeQueue) ? 1 : 0; -} - -OSU_EXPORT unsigned char nVulkanHasDedicatedTransferQueue(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().hasDedicatedTransferQueue) ? 1 : 0; -} - -OSU_EXPORT unsigned char nVulkanSupportsMailboxPresentMode(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().supportsMailboxPresentMode) ? 1 : 0; -} - -OSU_EXPORT unsigned char nVulkanMeetsVulkan13(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().meetsVulkan13) ? 1 : 0; -} - -OSU_EXPORT unsigned char nVulkanSupportsDynamicRendering(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().supportsDynamicRendering) ? 1 : 0; -} - -OSU_EXPORT unsigned char nVulkanSupportsSynchronization2(intptr_t ptr) { - auto* probe = reinterpret_cast(ptr); - return (probe && probe->getDeviceInfo().supportsSynchronization2) ? 1 : 0; -} - -} // extern "C" diff --git a/osu.Android/Native/vulkan_bridge.h b/osu.Android/Native/vulkan_bridge.h index b03701a7ecfa..0d867a129393 100644 --- a/osu.Android/Native/vulkan_bridge.h +++ b/osu.Android/Native/vulkan_bridge.h @@ -10,9 +10,6 @@ /// Lightweight Vulkan capability probe for Android. /// Requires Vulkan 1.3 as minimum for full feature detection (dynamic rendering, /// synchronization2). Falls back gracefully on older devices. -/// Does NOT create a full rendering pipeline — it checks device support -/// and reports GPU capabilities so the game can make informed decisions -/// about rendering strategy and low-latency presentation. class VulkanProbe { public: struct DeviceInfo { @@ -21,35 +18,28 @@ class VulkanProbe { uint32_t driverVersion = 0; uint32_t vendorId = 0; bool supportsSwapchain = false; - /// Total device-local memory in megabytes. uint32_t deviceLocalMemoryMB = 0; - /// Number of queue families available. uint32_t queueFamilyCount = 0; - /// Whether the device has a dedicated compute queue (separate from graphics). bool hasDedicatedComputeQueue = false; - /// Whether the device has a dedicated transfer queue. bool hasDedicatedTransferQueue = false; - /// Whether the device likely supports VK_PRESENT_MODE_MAILBOX_KHR for low-latency rendering. - /// Detected via the VK_GOOGLE_display_timing device extension, which is present on - /// Android GPUs (Adreno, Mali) that also expose MAILBOX present mode. bool supportsMailboxPresentMode = false; - /// Whether the device reports Vulkan 1.3+ API version. bool meetsVulkan13 = false; - /// Whether VkPhysicalDeviceVulkan13Features::dynamicRendering is supported. - /// Dynamic rendering eliminates VkRenderPass/VkFramebuffer boilerplate. bool supportsDynamicRendering = false; - /// Whether VkPhysicalDeviceVulkan13Features::synchronization2 is supported. - /// Provides a cleaner, less error-prone synchronization model. bool supportsSynchronization2 = false; + + // API 31+ / Modern High-Performance Extensions + bool supportsPresentId = false; + bool supportsPresentWait = false; + bool supportsGraphicsPipelineLibrary = false; + bool supportsShaderObject = false; + bool supportsGlobalPriority = false; + bool supportsMemoryBudget = false; }; VulkanProbe(); ~VulkanProbe(); - /// Returns true if Vulkan is available on this device. bool isAvailable() const { return available_; } - - /// Returns info about the selected physical device. const DeviceInfo& getDeviceInfo() const { return deviceInfo_; } private: @@ -63,5 +53,6 @@ class VulkanProbe { void queryQueueFamilies(VkPhysicalDevice device); void queryMailboxSupport(VkPhysicalDevice device); void queryVulkan13Features(VkPhysicalDevice device); + void queryModernExtensions(VkPhysicalDevice device); void cleanup(); }; diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index c5657bc46fa9..a1ed25af2fb6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using Android.Content; using Android.Media; using System; using System.Linq; @@ -149,7 +150,7 @@ protected override void LoadComplete() int hardwareSampleRate = 0; try { - if (gameActivity.GetSystemService(Android.Content.Context.AudioService) is AudioManager audioManager) + if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager) { string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate); if (!string.IsNullOrEmpty(rateStr)) @@ -317,7 +318,7 @@ private void startOboeBridge(Action onLatencyMeasured, IntPtr provider, try { - if (gameActivity.GetSystemService(Android.Content.Context.AudioService) is AudioManager audioManager) + if (gameActivity.GetSystemService(Context.AudioService) is AudioManager audioManager) { string? rateStr = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate); if (!string.IsNullOrEmpty(rateStr)) diff --git a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml index f3e8aefe0fea..708d62259430 100644 --- a/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Catch.Tests.Android/AndroidManifest.xml @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml index 79d1b1b99df1..1457b96af834 100644 --- a/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Mania.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml index 43f09bf97dac..35c5b125fcba 100644 --- a/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Osu.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml index 2ec118692332..7efab8626ff0 100644 --- a/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Rulesets.Taiko.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/osu.Game.Tests.Android/AndroidManifest.xml b/osu.Game.Tests.Android/AndroidManifest.xml index 3dd9c29b821d..69a889c1a714 100644 --- a/osu.Game.Tests.Android/AndroidManifest.xml +++ b/osu.Game.Tests.Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + \ No newline at end of file