Skip to content

Commit 44aa5c7

Browse files
authored
Merge pull request #345 from winnerspiros/copilot/optimize-performance-and-latency-again
perf: reduce allocations in beatmap conversion, playable bounds, and HUD components
2 parents 4f913fd + 9695c21 commit 44aa5c7

28 files changed

Lines changed: 311 additions & 117 deletions

osu.Android/Input/AndroidKeyboardHandler.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public class AndroidKeyboardHandler : InputHandler
5454
{ Keycode.Period, Key.Period }, { Keycode.Slash, Key.Slash },
5555
}.ToFrozenDictionary();
5656

57+
// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
58+
// chain; a volatile field gives the OS dispatch thread a direct read that is also
59+
// cross-thread correct.
60+
private volatile bool cachedEnabled = true;
61+
5762
public AndroidKeyboardHandler()
5863
{
5964
Enabled.Default = true;
@@ -65,13 +70,15 @@ public override bool Initialize(GameHost host)
6570
if (!base.Initialize(host))
6671
return false;
6772

73+
Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);
74+
6875
return true;
6976
}
7077

7178
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7279
public bool HandleKeyEvent(KeyEvent e)
7380
{
74-
if (!Enabled.Value) return false;
81+
if (!cachedEnabled) return false;
7582

7683
if (e.KeyCode == Keycode.Back || e.KeyCode == Keycode.Home || e.KeyCode == Keycode.Menu ||
7784
e.KeyCode == Keycode.VolumeUp || e.KeyCode == Keycode.VolumeDown || e.KeyCode == Keycode.VolumeMute ||

osu.Android/Input/AndroidMouseHandler.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ public class AndroidMouseHandler : InputHandler, IHasCursorSensitivity
5454
// Avoids a BindableDouble read per MotionEvent.
5555
private volatile float cachedSensitivity = 1f;
5656

57+
// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
58+
// chain; a volatile field gives the OS dispatch thread a direct read that is also
59+
// cross-thread correct (changes written on the Update thread are immediately
60+
// visible here via the volatile acquire/release semantics).
61+
private volatile bool cachedEnabled = true;
62+
5763
public AndroidMouseHandler()
5864
{
5965
Enabled.Default = true;
@@ -66,14 +72,15 @@ public override bool Initialize(GameHost host)
6672
return false;
6773

6874
Sensitivity.BindValueChanged(v => cachedSensitivity = (float)v.NewValue, true);
75+
Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);
6976

7077
return true;
7178
}
7279

7380
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7481
public bool HandleMotionEvent(MotionEvent e)
7582
{
76-
if (!Enabled.Value) return false;
83+
if (!cachedEnabled) return false;
7784

7885
if (e.ActionMasked == MotionEventActions.Scroll)
7986
{

osu.Android/Input/AndroidStylusHandler.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public class AndroidStylusHandler : InputHandler, ITabletHandler
9191
private bool useRotation;
9292
private float cachedPressureThreshold;
9393

94+
// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
95+
// chain; a volatile field gives the OS dispatch thread a direct read that is also
96+
// cross-thread correct.
97+
private volatile bool cachedEnabled = true;
98+
9499
// Cached tablet bounds — updated whenever `tablet.Value` is reassigned. Avoids
95100
// three bindable reads + property accesses per historical pointer sample in the
96101
// hot path. A local-field comparison is a single un-locked memory read.
@@ -178,6 +183,7 @@ public override bool Initialize(GameHost host)
178183

179184
Rotation.BindValueChanged(_ => updateCachedTransform());
180185
PressureThreshold.BindValueChanged(v => cachedPressureThreshold = v.NewValue, true);
186+
Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);
181187

182188
// Force one initial cache population so `areaWidth` / `outWidth` are non-zero
183189
// before the very first MotionEvent arrives (BindValueChanged above only fires
@@ -285,7 +291,7 @@ private void updateCachedTransform()
285291
[MethodImpl(MethodImplOptions.AggressiveInlining)]
286292
public bool HandleMotionEvent(MotionEvent e)
287293
{
288-
if (!Enabled.Value) return false;
294+
if (!cachedEnabled) return false;
289295

290296
// Cache ActionMasked once: each `e.ActionMasked` access is a JNI call into
291297
// MotionEvent#getActionMasked. On a busy stylus drag the previous code did

osu.Android/OsuGameAndroid.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ public partial class OsuGameAndroid : OsuGame
179179
private IntPtr adpfDrawSession;
180180
private IntPtr adpfUpdateSession;
181181

182+
// Cached GameThread references stored when ADPF sessions are created. Avoids walking
183+
// Host?.DrawThread? / Host?.UpdateThread? / Host?.InputThread? on every frame callback
184+
// (3 levels of nullable dereference at 120 Hz × 3 threads = 360 null checks/s → 0).
185+
private osu.Framework.Threading.GameThread? adpfDrawThread;
186+
private osu.Framework.Threading.GameThread? adpfUpdateThread;
187+
private osu.Framework.Threading.GameThread? adpfInputThread;
188+
182189
// Input thread ADPF session. The input thread may poll at ~100 kHz (one cycle ≈ 10 µs),
183190
// which is far finer than the ADPF reporting granularity. Instead of calling
184191
// reportActualWorkDuration once per poll (which would flood the ADPF API), we accumulate
@@ -603,6 +610,9 @@ protected override void LoadComplete()
603610
// Silent no-op on API < 35 (resolved via dlsym).
604611
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfDrawSession, 0);
605612
Logger.Log($"[osu!] ADPF session created for Draw thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
613+
// Cache the thread reference now (we're already on the Draw thread)
614+
// so per-frame callbacks avoid the Host?.DrawThread? nullable chain.
615+
adpfDrawThread = Host!.DrawThread;
606616
// Subscribe per-frame reporting now that the session handle is valid.
607617
// FrameCompleted fires on the Draw thread itself, so reading
608618
// Host.DrawThread.Clock.ElapsedFrameTime is thread-safe.
@@ -622,6 +632,7 @@ protected override void LoadComplete()
622632
{
623633
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfUpdateSession, 0);
624634
Logger.Log($"[osu!] ADPF session created for Update thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
635+
adpfUpdateThread = Host!.UpdateThread;
625636
Host!.UpdateThread!.FrameCompleted += onUpdateFrameCompleted;
626637
}
627638
}
@@ -643,6 +654,7 @@ protected override void LoadComplete()
643654
{
644655
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfInputSession, 0);
645656
Logger.Log($"[osu!] ADPF session created for Input thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
657+
adpfInputThread = Host!.InputThread;
646658
Host!.InputThread!.FrameCompleted += onInputFrameCompleted;
647659
}
648660
}
@@ -1607,7 +1619,7 @@ private void onDrawFrameCompleted()
16071619

16081620
try
16091621
{
1610-
double elapsedMs = Host?.DrawThread?.Clock.ElapsedFrameTime ?? 0;
1622+
double elapsedMs = adpfDrawThread?.Clock.ElapsedFrameTime ?? 0;
16111623
if (elapsedMs > 0)
16121624
OboeAudioBridge.nADPFReportActualDuration(adpfDrawSession, (long)(elapsedMs * 1_000_000.0));
16131625
}
@@ -1624,7 +1636,7 @@ private void onUpdateFrameCompleted()
16241636

16251637
try
16261638
{
1627-
double elapsedMs = Host?.UpdateThread?.Clock.ElapsedFrameTime ?? 0;
1639+
double elapsedMs = adpfUpdateThread?.Clock.ElapsedFrameTime ?? 0;
16281640
if (elapsedMs > 0)
16291641
OboeAudioBridge.nADPFReportActualDuration(adpfUpdateSession, (long)(elapsedMs * 1_000_000.0));
16301642
}
@@ -1644,7 +1656,7 @@ private void onInputFrameCompleted()
16441656

16451657
try
16461658
{
1647-
double elapsedMs = Host?.InputThread?.Clock.ElapsedFrameTime ?? 0;
1659+
double elapsedMs = adpfInputThread?.Clock.ElapsedFrameTime ?? 0;
16481660
if (elapsedMs <= 0) return;
16491661

16501662
inputAdpfAccumulatedMs += elapsedMs;

osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using osu.Game.Rulesets.Difficulty.Preprocessing;
87
using osu.Game.Rulesets.Difficulty.Utils;
98
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
@@ -116,12 +115,21 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
116115
if (previousIsland.DeltaCount == island.DeltaCount)
117116
effectiveRatio *= 0.5;
118117

119-
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
118+
int islandCountIndex = -1;
119+
(Island Island, int Count) islandCount = default;
120120

121-
if (islandCount != default)
121+
for (int k = 0; k < islandCounts.Count; k++)
122122
{
123-
int countIndex = islandCounts.IndexOf(islandCount);
123+
if (islandCounts[k].Island.Equals(island))
124+
{
125+
islandCountIndex = k;
126+
islandCount = islandCounts[k];
127+
break;
128+
}
129+
}
124130

131+
if (islandCountIndex >= 0)
132+
{
125133
// only add island to island counts if they're going one after another
126134
if (previousIsland.Equals(island))
127135
islandCount.Count++;
@@ -130,7 +138,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
130138
double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
131139
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
132140

133-
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
141+
islandCounts[islandCountIndex] = (islandCount.Island!, islandCount.Count);
134142
}
135143
else
136144
{

osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using osu.Game.Rulesets.Difficulty.Preprocessing;
87
using osu.Game.Rulesets.Difficulty.Utils;
98
using osu.Game.Rulesets.Mods;
@@ -63,7 +62,18 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnly
6362
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
6463
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);
6564

66-
if (mods.OfType<OsuModAutopilot>().Any())
65+
bool hasAutopilot = false;
66+
67+
for (int i = 0; i < mods.Count; i++)
68+
{
69+
if (mods[i] is OsuModAutopilot)
70+
{
71+
hasAutopilot = true;
72+
break;
73+
}
74+
}
75+
76+
if (hasAutopilot)
6777
distanceBonus = 0;
6878

6979
// Base difficulty with all bonuses

osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using osu.Game.Beatmaps;
87
using osu.Game.Rulesets.Difficulty;
98
using osu.Game.Rulesets.Difficulty.Preprocessing;
@@ -51,10 +50,36 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
5150
if (beatmap.HitObjects.Count == 0)
5251
return new OsuDifficultyAttributes { Mods = mods };
5352

54-
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
55-
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
56-
var speed = skills.OfType<Speed>().Single();
57-
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
53+
Aim? aim = null;
54+
Aim? aimWithoutSliders = null;
55+
Speed? speed = null;
56+
Flashlight? flashlight = null;
57+
58+
for (int i = 0; i < skills.Length; i++)
59+
{
60+
switch (skills[i])
61+
{
62+
case Aim a when a.IncludeSliders:
63+
aim = a;
64+
break;
65+
66+
case Aim a:
67+
aimWithoutSliders = a;
68+
break;
69+
70+
case Speed s:
71+
speed = s;
72+
break;
73+
74+
case Flashlight f:
75+
flashlight = f;
76+
break;
77+
}
78+
}
79+
80+
ArgumentNullException.ThrowIfNull(aim);
81+
ArgumentNullException.ThrowIfNull(aimWithoutSliders);
82+
ArgumentNullException.ThrowIfNull(speed);
5883

5984
double speedNotes = speed.RelevantNoteCount();
6085

@@ -74,9 +99,25 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
7499
double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate);
75100
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);
76101

77-
int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
78-
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
79-
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
102+
int hitCircleCount = 0, sliderCount = 0, spinnerCount = 0;
103+
104+
for (int i = 0; i < beatmap.HitObjects.Count; i++)
105+
{
106+
switch (beatmap.HitObjects[i])
107+
{
108+
case HitCircle:
109+
hitCircleCount++;
110+
break;
111+
112+
case Slider:
113+
sliderCount++;
114+
break;
115+
116+
case Spinner:
117+
spinnerCount++;
118+
break;
119+
}
120+
}
80121

81122
int totalHits = beatmap.HitObjects.Count;
82123

@@ -186,7 +227,18 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clo
186227
new Speed(mods)
187228
};
188229

189-
if (mods.Any(h => h is OsuModFlashlight))
230+
bool hasFlashlight = false;
231+
232+
for (int i = 0; i < mods.Length; i++)
233+
{
234+
if (mods[i] is OsuModFlashlight)
235+
{
236+
hasFlashlight = true;
237+
break;
238+
}
239+
}
240+
241+
if (hasFlashlight)
190242
skills.Add(new Flashlight(mods));
191243

192244
return skills.ToArray();

osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7-
using System.Linq;
87
using System.Numerics;
98
using osu.Framework.Graphics;
109
using osu.Framework.Utils;
@@ -308,7 +307,7 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
308307
var previousActions = previousFrame.Actions;
309308

310309
// If a button is already held, then we simply alternate
311-
if (previousActions.Any())
310+
if (previousActions.Count > 0)
312311
{
313312
// Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button.
314313
if (previousActions.Contains(action))
@@ -330,7 +329,7 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
330329
var frame = (OsuReplayFrame)Frames[j];
331330

332331
// Don't affect frames which stop pressing a button!
333-
if (j < Frames.Count - 1 || frame.Actions.SequenceEqual(previousActions))
332+
if (j < Frames.Count - 1 || actionsEqual(frame.Actions, previousActions))
334333
{
335334
frame.Actions.Clear();
336335
frame.Actions.Add(action);
@@ -395,6 +394,18 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
395394
AddFrameToReplay(endFrame);
396395
}
397396

397+
private static bool actionsEqual(List<OsuAction> a, List<OsuAction> b)
398+
{
399+
if (a.Count != b.Count) return false;
400+
401+
for (int i = 0; i < a.Count; i++)
402+
{
403+
if (a[i] != b[i]) return false;
404+
}
405+
406+
return true;
407+
}
408+
398409
#endregion
399410

400411
private class OsuKeyUpReplayFrame : OsuReplayFrame

osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs

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

44
using System.Collections.Generic;
5-
using System.Linq;
65
using osu.Framework.Input.StateChanges;
76
using osu.Framework.Utils;
87
using osu.Game.Replays;
@@ -17,7 +16,7 @@ public OsuFramedReplayInputHandler(Replay replay)
1716
{
1817
}
1918

20-
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
19+
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Count > 0;
2120

2221
protected override void CollectReplayInputs(List<IInput> inputs)
2322
{

0 commit comments

Comments
 (0)