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
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.502.3" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.502.4" />
<!-- `ppy.osu.Framework.NativeLibs` is a transitive dependency of `ppy.osu.Framework`
that ships desktop-only natives (Linux/macOS/Windows) under `runtimes/<rid>/native/`
— including a bare Linux `libbass.so`/`libbass_fx.so`/`libbassmix.so` for linux-arm64.
Expand Down
101 changes: 96 additions & 5 deletions osu.Android/CrashDiagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -485,11 +485,21 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
}
}

// Bound the read used by DetectPreviousDrawThreadNativeCrash. The native crash
// block + register dump + backtrace is ~2 KB; 256 KiB easily covers the most
// recent block even when the file also contains many ALIVE markers and a
// HangWatchdog dump from the previous process.
private const long crash_log_scan_byte_cap = 256L * 1024;
// Bound the read used by DetectPreviousDrawThreadNativeCrash.
//
// Footer path (new, fast): crash_handler.cpp appends a compact
// "=== CRASH FOOTER ===" line AFTER the /proc/self/maps dump, so it
// always lands in the last ~1 KiB of the log. We scan only the last
// crash_log_footer_scan_byte_cap bytes to find it quickly.
//
// Header-block fallback (legacy): older builds without the footer
// require scanning far enough back to reach the "[osu!] NATIVE CRASH"
// marker, which can be hundreds of KiB from the end because the
// /proc/self/maps section is typically 400–500 KiB on Android. The
// log is bounded at ~3 MiB by the rotation logic, so 4 MiB covers
// the entire file in the worst case.
private const long crash_log_footer_scan_byte_cap = 32L * 1024;
private const long crash_log_scan_byte_cap = 4L * 1024 * 1024;

/// <summary>
/// Inspect the on-disk <c>native_crash.log</c> for the most recent
Expand Down Expand Up @@ -534,6 +544,25 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
string path = Path.Combine(dir, CRASH_LOG_NAME);
if (!File.Exists(path)) return null;

// --- Fast path: look for the compact footer line appended by
// crash_handler.cpp after the /proc/self/maps dump. It is
// always in the last few KiB of the log, so a small read is
// enough. Falls through to the legacy full-header scan if the
// footer is absent (older native builds).
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
long footerStart = Math.Max(0, fs.Length - crash_log_footer_scan_byte_cap);
fs.Seek(footerStart, SeekOrigin.Begin);
using var sr = new StreamReader(fs);
string footerTail = sr.ReadToEnd();

var fromFooter = tryParseFooter(footerTail);
if (fromFooter != null) return fromFooter;
}

// --- Legacy path: the full-header "[osu!] NATIVE CRASH" block.
// The /proc/self/maps section can be 400–500 KiB, so we scan
// the last 4 MiB (the log rotation cap) to guarantee we reach it.
string tail;

using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
Expand Down Expand Up @@ -598,6 +627,68 @@ public DrawThreadNativeCrashInfo(string fingerprint, string signal, string threa
}
}

// Parse the compact "=== CRASH FOOTER ===" line appended by crash_handler.cpp
// after the /proc/self/maps section. Format:
// === CRASH FOOTER sig=<SIGNAL> pid=<PID> uptime_ns=<NS> thread=<NAME> ===
// Returns null if no valid footer line is found or the crash is not a
// fatal Draw-thread event.
private static DrawThreadNativeCrashInfo? tryParseFooter(string tail)
{
const string footer_marker = "=== CRASH FOOTER ";
int last = tail.LastIndexOf(footer_marker, StringComparison.Ordinal);
if (last < 0) return null;

int lineEnd = tail.IndexOf('\n', last);
if (lineEnd < 0) lineEnd = tail.Length;
string line = tail.Substring(last, lineEnd - last);

string? signal = extractFooterField(line, " sig=", " ");
string? pid = extractFooterField(line, " pid=", " ");
string? uptime = extractFooterField(line, " uptime_ns=", " ");
string? thread = extractFooterField(line, " thread=", " ===");

if (signal == null || thread == null) return null;

bool isFatalSignal = signal.StartsWith("SIGSEGV", StringComparison.Ordinal)
|| signal.StartsWith("SIGBUS", StringComparison.Ordinal)
|| signal.StartsWith("SIGABRT", StringComparison.Ordinal);
if (!isFatalSignal) return null;

if (!thread.StartsWith("Draw", StringComparison.Ordinal)) return null;

string fingerprint = (uptime != null && pid != null)
? $"u{uptime}-p{pid}"
: "footer:" + ((uint)line.GetHashCode()).ToString("x");

// The footer does not carry a top-frame symbol — report it as such.
return new DrawThreadNativeCrashInfo(fingerprint, signal, thread, "(footer — no top frame)");
}

// Extract a field value from a single footer line.
// Reads from after `key` to either the first occurrence of `stopBefore`
// or the end of the line (whichever comes first).
private static string? extractFooterField(string line, string key, string? stopBefore)
{
int idx = line.IndexOf(key, StringComparison.Ordinal);
if (idx < 0) return null;

int start = idx + key.Length;
int end;

if (stopBefore != null)
{
end = line.IndexOf(stopBefore, start, StringComparison.Ordinal);
if (end < 0) end = line.Length;
}
else
{
end = line.IndexOf(' ', start);
if (end < 0) end = line.Length;
}

return line.Substring(start, end - start);
}

private static string? extractField(string block, string keyWithEquals)
{
int idx = block.IndexOf(keyWithEquals, StringComparison.Ordinal);
Expand Down
50 changes: 36 additions & 14 deletions osu.Android/Native/crash_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,21 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) {
}
g_dumpWritten = 1;

// Capture crash metadata once so both the header block and the compact
// footer written after /proc/self/maps use identical values. The footer
// is what CrashDiagnostics.scanForDrawThreadCrash now looks for first —
// it always lands in the last few KB of the log even when the memory-map
// section is several hundred KB long.
long long crash_uptime_ns;
{
struct timespec ts{};
clock_gettime(CLOCK_BOOTTIME, &ts);
crash_uptime_ns = (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec;
}
const long long crash_pid = (long long)getpid();
char crash_thread_name[32] = {};
(void)pthread_getname_np(pthread_self(), crash_thread_name, sizeof(crash_thread_name));

// Open the dump file (append). If g_logPath is empty we still log to logcat.
//
// Pre-rotate runaway: if the existing log is more than 4× the soft cap
Expand Down Expand Up @@ -1051,21 +1066,9 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) {
writeStr(fd, "\n pid = ");
writeDec(fd, (long long)getpid());
writeStr(fd, "\n uptime_ns = ");
{
struct timespec ts;
clock_gettime(CLOCK_BOOTTIME, &ts);
writeDec(fd, (long long)ts.tv_sec * 1000000000LL + (long long)ts.tv_nsec);
}
writeDec(fd, crash_uptime_ns);
writeStr(fd, "\n thread_name = ");
{
char name[32] = {};
// pthread_getname_np is signal-safe in bionic (it's a thin wrapper
// over a /proc/self/task/<tid>/comm read).
if (pthread_getname_np(pthread_self(), name, sizeof(name)) == 0)
writeStr(fd, name);
else
writeStr(fd, "?");
}
writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?");
writeStr(fd, "\n");

// Logcat header (so users with logcat access also see something useful).
Expand Down Expand Up @@ -1137,6 +1140,25 @@ static void crashHandler(int sig, siginfo_t* info, void* ucontext) {
writeStr(fd, "=========================================================\n");
writeStr(fd, "=== END OF CRASH DUMP ===\n");

// Compact one-line footer, written AFTER the /proc/self/maps section.
// The C# scanner (CrashDiagnostics.scanForDrawThreadCrash) looks for this
// footer FIRST in the last 32 KiB of the log. Without it the scanner
// would need to scan hundreds of KiB backwards past the memory map just to
// reach the "[osu!] NATIVE CRASH" header — causing safe-mode detection to
// silently fail and the app to keep relaunching into the same Vulkan crash.
// The uptime_ns and pid values here MATCH the header block exactly (both
// were captured at handler entry above) so the fingerprint computed by the
// C# scanner is identical regardless of which block it reads.
writeStr(fd, "=== CRASH FOOTER sig=");
writeStr(fd, signalName(sig));
writeStr(fd, " pid=");
writeDec(fd, crash_pid);
writeStr(fd, " uptime_ns=");
writeDec(fd, crash_uptime_ns);
writeStr(fd, " thread=");
writeStr(fd, crash_thread_name[0] ? crash_thread_name : "?");
writeStr(fd, " ===\n");

if (fd >= 0) {
fsync(fd);
close(fd);
Expand Down
2 changes: 1 addition & 1 deletion osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ protected override void OnCreate(Bundle? savedInstanceState)
}
else
{
Logger.Log("[osu!] SurfaceHolder.SetFormat skipped (OpenGL/Auto renderer — SDL3 handles format).", LoggingTarget.Runtime, LogLevel.Important);
Logger.Log("[osu!] SurfaceHolder.SetFormat skipped (OpenGL/Auto renderer — SDL3 handles format).", LoggingTarget.Runtime, LogLevel.Debug);
}

holder.AddCallback(this);
Expand Down
4 changes: 2 additions & 2 deletions osu.Game/osu.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.502.3" />
<PackageReference Include="ppy.osu.Framework" Version="2026.502.4" />
<!--
Explicitly pin `ppy.Veldrid.SPIRV` to the winnerspiros fork build that
`ppy.osu.Framework 2026.502.3` was compiled against. This version is the only
`ppy.osu.Framework 2026.502.4` was compiled against.This version is the only
one whose `runtimes/android-arm64/native/libveldrid-spirv.so` is aligned to 16 KB
pages (required by Android 16+). It lives only as a release asset on
<https://github.com/winnerspiros/veldrid-spirv/releases/tag/1.0> and is vendored
Expand Down
2 changes: 1 addition & 1 deletion osu.iOS.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.502.3" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2026.502.4" />
</ItemGroup>
</Project>
Loading