Skip to content
Merged

tmp #348

Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e85ac22
Update framework
peppy May 21, 2026
5b24de3
Update resources
peppy May 21, 2026
a31eadc
Add session-level playable beatmap cache + persisted slider pool meta…
Copilot May 21, 2026
89fe51a
Add versioned playable prewarm and persisted slider ticks metadata
Copilot May 21, 2026
6cfec93
Fix double GetPlayableBeatmap in BeatmapUpdater; start prewarm in Loa…
Copilot May 21, 2026
172e66e
Gameplay perf: ISliderTick interface, hoist PositionAt out of repeat …
Copilot May 21, 2026
0388d4a
5 gameplay hot-path optimizations: snaking threshold, slider ball cac…
Copilot May 21, 2026
30c49d1
Address code-review: ArgonAccuracyCounter format fallback, Playfield …
Copilot May 21, 2026
9f4834f
Fix CI errors: Logger import, enumerateByStartTimeAscending, Playable…
Copilot May 21, 2026
6affa8a
Optimize slider/spinner hot paths: cache SliderBody, null-safe repeat…
Copilot May 21, 2026
6be767c
Perf: RDP slider path simplification (3-10x fewer render vertices) + …
Copilot May 21, 2026
9fddd91
Merge remote-tracking branch 'upstream/master' into copilot/add-persi…
Copilot May 21, 2026
ca2faed
fix: remove nullable annotation from Slider cache field
Copilot May 22, 2026
4504ae5
fix: InspectCode formatting warnings + HSPAColour perceived-brightnes…
Copilot May 22, 2026
cdc4cbf
fix: add general Vulkan UI-thread watchdog for SDL surface-event ANR …
Copilot May 22, 2026
eab2b49
fix: resolve Android watchdog namespace/Environment compile errors
Copilot May 22, 2026
dceaa0a
fix: correct SnakingSliderBody.cs line 167-168 continuation indent fr…
Copilot May 22, 2026
d1cd7c5
fix: satisfy InspectCode WrongIndentSize at SnakingSliderBody line 167
Copilot May 22, 2026
ea3f5ac
ci: raise test job timeout to 120 minutes
Copilot May 22, 2026
805167c
fix: avoid startup backfill for slider pool metadata
Copilot May 22, 2026
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 @@ -52,7 +52,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.520.3" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.521.1" />
<!-- `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
137 changes: 137 additions & 0 deletions osu.Android/OsuGameActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,11 @@
}

CrashDiagnostics.WriteAliveMarker("Activity.OnCreate exit");

// Arm the general Vulkan UI-thread watchdog LAST in OnCreate, after all
// surface setup and base initialisation is complete. The 7-second initial
// sleep in the watchdog thread provides additional grace.
startVulkanUiWatchdog();
}

protected override void OnNewIntent(Intent? intent) => handleIntent(intent);
Expand Down Expand Up @@ -967,6 +972,138 @@
}
}

/// <summary>
/// Arms a background watchdog that monitors the Java main thread (UI thread) for
/// responsiveness during Vulkan sessions.
///
/// <para>
/// Root cause this addresses: SDL3 on Android serialises certain surface-lifecycle
/// and window-focus events (surfaceDestroyed, nativeWindowFocusChanged, …) through
/// an internal Java <c>synchronized</c> block that blocks the Java main thread until
/// the SDL game/draw thread acknowledges the event. If the draw thread is stuck
/// inside a long kernel GPU driver call (e.g. <c>vkQueuePresentKHR</c> on an
/// Adreno 7xx under high load — visible as 70–80% kernel CPU time), the
/// acknowledgement is delayed and the main thread can block for the full 10-second
/// Android ANR window. The Samsung Game Optimizing Service (GOS) toolbar, ADPF
/// display-mode changes, and other system overlays are typical triggers around
/// 25–35 s of gameplay.
/// </para>
///
/// <para>
/// The <see cref="OnPause"/> watchdog already covers the <c>OnPause</c> path.
/// This watchdog covers every other blocking entry point on the UI thread by
/// posting a 1-second repeating no-op to the main looper and measuring how long
/// ago the last no-op was executed. If the gap exceeds 7 seconds (leaving a 3-
/// second margin before the 10-second ANR deadline), the process is killed for a
/// clean restart rather than an ANR.
/// </para>
///
/// <para>
/// Safe-mode is deliberately NOT set: this watchdog fires during mid-session
/// driver stalls, not at startup. The next launch should retry Vulkan normally.
/// </para>
/// </summary>
private void startVulkanUiWatchdog()
{
if (!LogManagement.IsVulkanConfigured())
return;

const int watchdog_threshold_ms = 7000;
const int ping_interval_ms = 1000;

Android.OS.Handler? pingHandler;

Check failure on line 1014 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1014 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1014 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1014 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

try
{
pingHandler = new Android.OS.Handler(Android.OS.Looper.MainLooper!);

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)

Check failure on line 1018 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

The type or namespace name 'OS' does not exist in the namespace 'osu.Android' (are you missing an assembly reference?)
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] VulkanUiWatchdog: failed to create Handler — watchdog disabled ({e.Message})");
return;
}

// Shared field written by the UI-thread pong and read by the watchdog thread.
// Interlocked/Volatile access: the pong runs on the UI thread, the reader runs
// on the watchdog thread. The field is a reference so it can be captured by
// both lambdas without a ref capture.
long[] lastPongMonotonicMs = { Environment.TickCount64 };

Check failure on line 1030 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

Check failure on line 1030 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

// Self-rescheduling pong: posts itself every ping_interval_ms on the UI thread.
// Capturing pingHandler via a local ref rather than the outer variable so the
// lambda holds no hard reference to the activity (GC-safety: the Handler is
// bound to the process-lifetime main looper, not to this activity instance).
Action? pong = null;
pong = () =>
{
System.Threading.Volatile.Write(ref lastPongMonotonicMs[0], Environment.TickCount64);

Check failure on line 1039 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

Check failure on line 1039 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

try { pingHandler.PostDelayed(pong!, ping_interval_ms); }

Check failure on line 1041 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'OS.Handler?' does not contain a definition for 'PostDelayed' and no accessible extension method 'PostDelayed' accepting a first argument of type 'OS.Handler?' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 1041 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'OS.Handler?' does not contain a definition for 'PostDelayed' and no accessible extension method 'PostDelayed' accepting a first argument of type 'OS.Handler?' could be found (are you missing a using directive or an assembly reference?)
catch { /* handler gone (process teardown) — stop rescheduling */ }
};

try
{
pingHandler.Post(pong);

Check failure on line 1047 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'OS.Handler?' does not contain a definition for 'Post' and no accessible extension method 'Post' accepting a first argument of type 'OS.Handler?' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 1047 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'OS.Handler?' does not contain a definition for 'Post' and no accessible extension method 'Post' accepting a first argument of type 'OS.Handler?' could be found (are you missing a using directive or an assembly reference?)
}
catch (Exception e)
{
Debug.WriteLine($"[osu!] VulkanUiWatchdog: failed to post initial pong — watchdog disabled ({e.Message})");
return;
}

var watchdog = new System.Threading.Thread(() =>
{
// Initial grace period equal to the threshold: the first pong may not
// have executed yet, and early-startup UI-thread work (surface format
// stamp, DecorView.Post lambda, Samsung Game Launcher broadcast) all
// runs during this window.
System.Threading.Thread.Sleep(watchdog_threshold_ms);

while (true)
{
long age = Environment.TickCount64 - System.Threading.Volatile.Read(ref lastPongMonotonicMs[0]);

Check failure on line 1065 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

Check failure on line 1065 in osu.Android/OsuGameActivity.cs

View workflow job for this annotation

GitHub Actions / Build only (Android)

'Environment' is an ambiguous reference between 'Android.OS.Environment' and 'System.Environment'

if (age > watchdog_threshold_ms)
{
// UI thread has not executed the pong for > 7 s. The draw thread
// is conclusively stuck in vkQueuePresentKHR (or a similar SDL
// surface-event wait), holding the Java main thread. Kill now to
// avoid the impending ANR and let the user's launcher restart cleanly.
try
{
CrashDiagnostics.WriteAliveMarker(
$"VulkanUiWatchdog fired after {age}ms: "
+ "main thread blocked in SDL surface-event wait — "
+ "draw thread stuck in vkQueuePresentKHR (Vulkan KGSL stall). "
+ "Killing for clean restart rather than Android ANR.");
}
catch { }

try
{
Debug.WriteLine(
$"[osu!] VulkanUiWatchdog: main thread blocked {age}ms >7s — killing for clean Vulkan restart.");
}
catch { }

try { global::Android.OS.Process.KillProcess(global::Android.OS.Process.MyPid()); }
catch { }

return;
}

System.Threading.Thread.Sleep(ping_interval_ms);
}
})
{
IsBackground = true,
Name = "VulkanUiWatchdog",
};

watchdog.Start();
}

protected override void OnPause()
{
// Root cause of the recurring Vulkan IMMEDIATE-mode ANR (process-runtime ~50s):
Expand Down
23 changes: 18 additions & 5 deletions osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,27 @@ protected override void UpdateAfterChildren()

double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);

// Cache once per frame: the property resolves Body.Drawable via a type-check cast on each access.
PlaySliderBody sliderBody = SliderBody;

Ball.UpdateProgress(completionProgress);
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
sliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);

// Pre-compute the snaking start/end path positions once.
// PositionAt() performs a binary search over the pre-built path points list,
// so recomputing it for every repeat on every frame is wasteful — all repeats
// share the same start/end values within a single update.
if (repeatContainer.Count > 0)
{
Vector2 snakeStart = HitObject.Path.PositionAt(sliderBody?.SnakedStart ?? 0);
Vector2 snakeEnd = HitObject.Path.PositionAt(sliderBody?.SnakedEnd ?? 0);

foreach (DrawableSliderRepeat repeat in repeatContainer)
repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
foreach (DrawableSliderRepeat repeat in repeatContainer)
repeat.UpdateSnakingPosition(snakeStart, snakeEnd);
}

Size = SliderBody?.Size ?? Vector2.Zero;
OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
Size = sliderBody?.Size ?? Vector2.Zero;
OriginPosition = sliderBody?.PathOffset ?? Vector2.Zero;

if (!relativeAnchorPositionLayout.IsValid)
{
Expand Down
27 changes: 22 additions & 5 deletions osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,34 @@ public override void ApplyTransformsAt(double time, bool propagateChildren = fal
base.ApplyTransformsAt(time, false);
}

private double cachedPathDistance = -1;
private double cachedCheckDistance;

public void UpdateProgress(double completionProgress)
{
Slider slider = drawableSlider.HitObject;

// Cache the check-distance; Path.Distance is stable after ApplyDefaults so the
// division only runs once per slider-pool reuse (when the path changes).
double pathDistance = slider.Path.Distance;

if (pathDistance != cachedPathDistance)
{
cachedPathDistance = pathDistance;
cachedCheckDistance = 0.1 / pathDistance;
}

// Exact position at current progress (binary search #1).
Position = slider.CurvePositionAt(completionProgress);

// 0.1 / slider.Path.Distance is the additional progress needed to ensure the diff length is 0.1
double checkDistance = 0.1 / slider.Path.Distance;
var diff = slider.CurvePositionAt(Math.Min(1 - checkDistance, completionProgress)) - slider.CurvePositionAt(Math.Min(1, completionProgress + checkDistance));
// Forward-tangent point for ball rotation (binary search #2).
// Using (current → forward) instead of the original symmetric
// (backward → forward) cuts one PositionAt call per frame with
// imperceptible accuracy loss, since checkDistance is tiny.
double dForward = Math.Min(1, completionProgress + cachedCheckDistance);
var diff = Position - slider.CurvePositionAt(dForward);

// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
// Needed for when near completion, or in case of a very short slider.
// Ensure the diff is long enough for Atan2 to return a meaningful angle.
if (diff.LengthSquared() < 0.0001f)
return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ public void UpdateSnakingPosition(Vector2 start, Vector2 end)
if (IsHit) return;

bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0;
List<Vector2> curve = ((PlaySliderBody)DrawableSlider.Body.Drawable).CurrentCurve;
List<Vector2> curve = DrawableSlider.SliderBody?.CurrentCurve;

Position = isRepeatAtEnd ? end : start;

if (curve.Count < 2)
if (curve == null || curve.Count < 2)
return;

Vector2 aimRotationVector = Vector2.Zero;
Expand Down
34 changes: 30 additions & 4 deletions osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public partial class DrawableSpinner : DrawableOsuHitObject
private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate;

/// <summary>
/// Scan index for the first unjudged nested tick, updated each frame.
/// Avoids restarting the <see cref="DrawableHitObject.NestedHitObjects"/> scan from index 0
/// every frame once many ticks have already been judged.
/// </summary>
private int nextUnjudgedTickIndex;

private double lastTickScanTime;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;

Expand Down Expand Up @@ -137,6 +146,9 @@ protected override void OnFree()

spinningSample.ClearSamples();
maxBonusSample.ClearSamples();

nextUnjudgedTickIndex = 0;
lastTickScanTime = double.MinValue;
}

protected override void LoadSamples()
Expand Down Expand Up @@ -291,13 +303,27 @@ protected override void Update()
// but for performance reasons, we only want to keep the next tick alive.
DrawableHitObject nextTick = null;

foreach (var nested in NestedHitObjects)
// Reset the hint index on rewind so newly-unjudged ticks are not skipped.
if (Time.Current < lastTickScanTime)
nextUnjudgedTickIndex = 0;

lastTickScanTime = Time.Current;

int nestedCount = NestedHitObjects.Count;

for (int i = nextUnjudgedTickIndex; i < nestedCount; i++)
{
if (!nested.Judged)
var nested = NestedHitObjects[i];

if (nested.Judged)
{
nextTick = nested;
break;
// Advance the hint past confirmed judged ticks.
nextUnjudgedTickIndex = i + 1;
continue;
}

nextTick = nested;
break;
}

// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
Expand Down
8 changes: 7 additions & 1 deletion osu.Game.Rulesets.Osu/Objects/Slider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ public double Duration
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}

public override IList<HitSampleInfo> AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
public override IList<HitSampleInfo> AuxiliarySamples => cachedAuxiliarySamples ??= CreateSlidingSamples().Concat(TailSamples).ToArray();

// Cached after ApplyDefaults populates TailSamples — stable for the lifetime of this slider instance.
private IList<HitSampleInfo> cachedAuxiliarySamples;

private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();

Expand Down Expand Up @@ -165,6 +168,9 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok
{
base.CreateNestedHitObjects(cancellationToken);

// Invalidate the auxiliary-samples cache since TailSamples will be reassigned below.
cachedAuxiliarySamples = null;

var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken);

foreach (var e in sliderEvents)
Expand Down
3 changes: 2 additions & 1 deletion osu.Game.Rulesets.Osu/Objects/SliderTick.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;

namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderTick : OsuHitObject
public class SliderTick : OsuHitObject, ISliderTick
{
public int SpanIndex { get; set; }
public double SpanStartTime { get; set; }
Expand Down
Loading
Loading