diff --git a/archive.zip b/archive.zip deleted file mode 100644 index ffb794c5ac3d..000000000000 Binary files a/archive.zip and /dev/null differ diff --git a/native_crash.log b/native_crash.log deleted file mode 100644 index f5b347eb902c..000000000000 --- a/native_crash.log +++ /dev/null @@ -1,97 +0,0 @@ -=== ALIVE [2026-04-22T20:07:05.4355817Z] Activity.OnCreate entry === -=== INSTALL_STATE sentinel=present log_path=/data/user/0/sh.ppy.osulazer/files/native_crash.log internal_dir=/data/user/0/sh.ppy.osulazer/files external_dir=/storage/emulated/0/Android/data/sh.ppy.osulazer/files === -=== ALIVE [2026-04-22T20:06:53.1523889Z] Activity.OnCreate exit === - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (TypeInitializationException) - utc_time = 2026-04-22T20:06:53.6451589Z - thread_id = 16 - thread_name= .NET TP Worker - -System.TypeInitializationException: TypeInitialization_Type, HidSharp.Platform.MacOS.NativeMethods - ---> System.EntryPointNotFoundException: CFStringCreateWithCharacters - at HidSharp.Platform.MacOS.NativeMethods.CFStringCreateWithCharacters(String str) - at HidSharp.Platform.MacOS.NativeMethods..cctor() - Exception_EndOfInnerExceptionStack -=== END OF MANAGED EXCEPTION === - -=== ALIVE [2026-04-22T20:06:53.7134269Z] OsuGameAndroid.SetHost (GameHost.Run entry) === -=== ALIVE [2026-04-22T20:06:53.7140169Z] CrashDiagnostics.ReinstallNativeHandler (chained on top of Mono) === -=== ALIVE [2026-04-22T20:06:53.8160453Z] OsuGameAndroid.SetHost (base.SetHost returned) === -=== ALIVE [2026-04-22T20:07:05.4355817Z] Activity.OnCreate entry === -=== INSTALL_STATE sentinel=present log_path=/data/user/0/sh.ppy.osulazer/files/native_crash.log internal_dir=/data/user/0/sh.ppy.osulazer/files external_dir=/storage/emulated/0/Android/data/sh.ppy.osulazer/files === -=== ALIVE [2026-04-22T20:07:05.4686997Z] Activity.OnCreate exit === - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (TypeInitializationException) - utc_time = 2026-04-22T20:07:05.9448287Z - thread_id = 17 - thread_name= .NET TP Worker - -System.TypeInitializationException: TypeInitialization_Type, HidSharp.Platform.MacOS.NativeMethods - ---> System.EntryPointNotFoundException: CFStringCreateWithCharacters - at HidSharp.Platform.MacOS.NativeMethods.CFStringCreateWithCharacters(String str) - at HidSharp.Platform.MacOS.NativeMethods..cctor() - Exception_EndOfInnerExceptionStack -=== END OF MANAGED EXCEPTION === - -=== ALIVE [2026-04-22T20:07:06.0273611Z] OsuGameAndroid.SetHost (GameHost.Run entry) === -=== ALIVE [2026-04-22T20:07:06.0279055Z] CrashDiagnostics.ReinstallNativeHandler (chained on top of Mono) === -=== ALIVE [2026-04-22T20:07:06.1205503Z] OsuGameAndroid.SetHost (base.SetHost returned) === - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (VeldridException) - utc_time = 2026-04-22T20:07:16.1441478Z - thread_id = 44 - thread_name= Draw (GameThread) - -Veldrid.VeldridException: The Swapchain's underlying surface has been lost. -=== END OF MANAGED EXCEPTION === - - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (VeldridException) - utc_time = 2026-04-22T20:07:16.1982648Z - thread_id = 44 - thread_name= Draw (GameThread) - -Veldrid.VeldridException: The Swapchain's underlying surface has been lost. -=== END OF MANAGED EXCEPTION === - - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (VeldridException) - utc_time = 2026-04-22T20:07:16.2001849Z - thread_id = 44 - thread_name= Draw (GameThread) - -Veldrid.VeldridException: The Swapchain's underlying surface has been lost. -=== END OF MANAGED EXCEPTION === - - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (VeldridException) - utc_time = 2026-04-22T20:07:16.2132417Z - thread_id = 44 - thread_name= Draw (GameThread) - -Veldrid.VeldridException: The Swapchain's underlying surface has been lost. -=== END OF MANAGED EXCEPTION === - - -========================================================= -=== MANAGED EXCEPTION === - source = FirstChanceException (VeldridException) - utc_time = 2026-04-22T20:07:16.2136365Z - thread_id = 44 - thread_name= Draw (GameThread) - -Veldrid.VeldridException: The Swapchain's underlying surface has been lost. -=== END OF MANAGED EXCEPTION === - diff --git a/osu.Android/CrashDiagnostics.cs b/osu.Android/CrashDiagnostics.cs index db5d393012bf..6f5b9359cd25 100644 --- a/osu.Android/CrashDiagnostics.cs +++ b/osu.Android/CrashDiagnostics.cs @@ -69,6 +69,12 @@ internal static class CrashDiagnostics /// Any — typically the host Activity. public static void InstallNativeHandler(Context context) { + // Idempotent at the managed level: the native handler dedupes via its own + // g_installed flag, but we also avoid re-writing the sentinel and re-running + // the directory-resolution / P-Invoke path on repeat calls. + if (Interlocked.Exchange(ref initialised, 1) != 0) + return; + try { resolveDirs(context); @@ -106,8 +112,6 @@ public static void InstallNativeHandler(Context context) { Debug.WriteLine($"[osu!] CrashDiagnostics.InstallNativeHandler outer failure: {e.Message}"); } - - Interlocked.Exchange(ref initialised, 1); } /// diff --git a/osu.Android/Native/oboe_bridge.cpp b/osu.Android/Native/oboe_bridge.cpp index 814ed330ee21..c326317a9991 100644 --- a/osu.Android/Native/oboe_bridge.cpp +++ b/osu.Android/Native/oboe_bridge.cpp @@ -196,6 +196,10 @@ bool OboeBridge::start() { } void OboeBridge::stop() { + // Signal any in-flight error-callback recovery (onErrorAfterClose → + // reopenAndRestart) to bail out, so the bridge cannot be reopened from + // Oboe's internal thread while we are tearing it down from .NET. + disposing_.store(true); active_.store(false); std::lock_guard lock(streamLock_); @@ -360,6 +364,16 @@ void OboeBridge::onErrorAfterClose(oboe::AudioStream* stream, oboe::Result error oboe::convertToText(error)); active_.store(false); + // Bail out immediately if a teardown is in flight: stop() has signalled + // that the bridge is being destroyed by .NET, and proceeding with the + // recovery path could leave us inside open()/requestStart() while the + // OboeBridge object is freed by the destructor. + if (disposing_.load()) { + std::lock_guard lock(streamLock_); + stream_.reset(); + return; + } + if (error == oboe::Result::ErrorDisconnected) { { std::lock_guard lock(streamLock_); @@ -378,9 +392,18 @@ void OboeBridge::onErrorAfterClose(oboe::AudioStream* stream, oboe::Result error } bool OboeBridge::reopenAndRestart() { + // Re-check teardown after acquiring no-lock fast path: stop() may have + // been called between onErrorAfterClose's check and now. + if (disposing_.load()) return false; + if (open(requestedSampleRate_)) { std::lock_guard lock(streamLock_); + if (disposing_.load()) { + stream_.reset(); + return false; + } + if (stream_) { oboe::Result result = stream_->requestStart(); diff --git a/osu.Android/Native/oboe_bridge.h b/osu.Android/Native/oboe_bridge.h index 0bb9a3b4744d..59bb57a98e19 100644 --- a/osu.Android/Native/oboe_bridge.h +++ b/osu.Android/Native/oboe_bridge.h @@ -53,6 +53,7 @@ class OboeBridge : public oboe::AudioStreamCallback { mutable std::mutex streamLock_; std::atomic active_{false}; + std::atomic disposing_{false}; std::atomic latencyMs_{-1.0}; std::atomic callbackCount_{0}; std::atomic provider_{nullptr}; diff --git a/osu.Android/Native/vulkan_bridge.cpp b/osu.Android/Native/vulkan_bridge.cpp index 9de3969e9e10..6a3d2f00a0e5 100644 --- a/osu.Android/Native/vulkan_bridge.cpp +++ b/osu.Android/Native/vulkan_bridge.cpp @@ -77,11 +77,12 @@ bool VulkanProbe::createInstance() { bool VulkanProbe::queryDevice() { uint32_t deviceCount = 0; - vkEnumeratePhysicalDevices(instance_, &deviceCount, nullptr); + if (vkEnumeratePhysicalDevices(instance_, &deviceCount, nullptr) != VK_SUCCESS) return false; if (deviceCount == 0) return false; std::vector devices(deviceCount); if (vkEnumeratePhysicalDevices(instance_, &deviceCount, devices.data()) != VK_SUCCESS) return false; + if (deviceCount == 0) return false; VkPhysicalDevice selected = devices[0]; for (const auto& dev : devices) { @@ -131,6 +132,7 @@ void VulkanProbe::queryQueueFamilies(VkPhysicalDevice device) { uint32_t count = 0; vkGetPhysicalDeviceQueueFamilyProperties(device, &count, nullptr); deviceInfo_.queueFamilyCount = count; + if (count == 0) return; std::vector families(count); vkGetPhysicalDeviceQueueFamilyProperties(device, &count, families.data()); for (const auto& family : families) { @@ -143,9 +145,10 @@ void VulkanProbe::queryQueueFamilies(VkPhysicalDevice device) { void VulkanProbe::queryModernExtensions(VkPhysicalDevice device) { uint32_t count = 0; - vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr); + if (vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr) != VK_SUCCESS) return; + if (count == 0) return; std::vector exts(count); - vkEnumerateDeviceExtensionProperties(device, nullptr, &count, exts.data()); + if (vkEnumerateDeviceExtensionProperties(device, nullptr, &count, exts.data()) != VK_SUCCESS) return; 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; diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 645cf11df115..285de8590374 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -90,7 +90,7 @@ public partial class OsuGameAndroid : OsuGame private object? nativeBridges; /// - /// Last value passed to by + /// Last value passed to by /// . Cached locally so we can short-circuit /// redundant updates without round-tripping through the activity getter, which /// itself performs a binder IPC on modern Android. diff --git a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs index fe5819bf1691..8809298f26a1 100644 --- a/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs +++ b/osu.Game/Screens/Play/HUD/ClicksPerSecond/ClicksPerSecondController.cs @@ -36,29 +36,42 @@ protected override void Update() double latestValidTime = clock.CurrentTime; double earliestTimeValid = latestValidTime - 1000 * gameplayClock.GetTrueGameplayRate(); - // Timestamps are added in chronological order (from clock.CurrentTime), - // so we can use binary-search-style trimming instead of per-element RemoveAt. + // Timestamps are appended at clock.CurrentTime which is *usually* monotonic, but + // gameplay rewinds (and replay seeks) can append a smaller value after a larger + // one — so the list is not strictly sorted. We still scan from the end (where + // newly-appended entries live) to match the access pattern of the previous + // implementation, but we cannot stop early on either bound because an older + // out-of-order entry may live anywhere in the list. + + // First pass: drop any timestamps now in the future (caused by rewinding). + // Walk backwards and shift surviving entries down in-place; this is O(n) and + // avoids the O(n²) RemoveAt-in-loop pattern of the original code. + int write = 0; + + for (int read = 0; read < timestamps.Count; read++) + { + double t = timestamps[read]; + + if (t > latestValidTime) + continue; - // Trim future timestamps caused by rewinding (remove from the end in one batch). - // RemoveRange from the end is a single operation vs repeated RemoveAt calls. - int trimStart = timestamps.Count; + if (write != read) + timestamps[write] = t; - while (trimStart > 0 && timestamps[trimStart - 1] > latestValidTime) - trimStart--; + write++; + } - if (trimStart < timestamps.Count) - timestamps.RemoveRange(trimStart, timestamps.Count - trimStart); + if (write < timestamps.Count) + timestamps.RemoveRange(write, timestamps.Count - write); - // Count timestamps within the valid 1-second window. - // Since the list is in chronological order, scan backwards until we leave the window. + // Count entries inside the 1-second window. Cannot break early because the list + // is not guaranteed sorted (see above), so scan all surviving timestamps. int count = 0; - for (int i = timestamps.Count - 1; i >= 0; i--) + for (int i = 0; i < timestamps.Count; i++) { - if (timestamps[i] < earliestTimeValid) - break; - - count++; + if (timestamps[i] >= earliestTimeValid) + count++; } Value = count;