Skip to content

perf: second-pass hot-path audit — MathF, DimmablePieces cache, Taiko loop - #308

Merged
winnerspiros merged 17 commits into
masterfrom
copilot/merge-ppy-prioritize-our-fork
May 8, 2026
Merged

perf: second-pass hot-path audit — MathF, DimmablePieces cache, Taiko loop#308
winnerspiros merged 17 commits into
masterfrom
copilot/merge-ppy-prioritize-our-fork

Conversation

Copilot AI commented May 7, 2026

Copy link
Copy Markdown
  • Fix InspectCode "Line break is missing elsewhere" warning in DrawableSlider.cs — wrap foreach body in braces and put if (h.IsHit) body on its own line

triacontakai and others added 7 commits May 7, 2026 08:59
…ection in multiplayer (ppy#37646)

Fixes a bug that allowed for selecting the same mod twice in multiplayer
if the playlist entry has non-default settings for a required mod.
This happens due to a strict equality mod check in the mod set
compatibility check function, which only considers mods duplicates if
their settings are exactly the same. Replacing with a more lenient
`Type` check fixes this.

Adds a regression test for this behavior

Fixes ppy#37625.

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
…e screen (ppy#37658)

Until now the queue screen basically did nothing to let the user knowing
they were disconnected from the server. Now the various components will
correctly clear state and show a roughly competent "i'm trying to
reconnect" state.



https://github.com/user-attachments/assets/bff1b241-a6a2-445a-9ffa-b5682f2a3656




---

Can be tested using the following patch (hit `F7` to reconnect, with a 5
second delay to show the disconnected state too):

```diff
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
index 7064906..ae539aba8d 100644
--- a/osu.Game/Online/PersistentEndpointClientConnector.cs
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -99,6 +99,8 @@ private async Task connect()
                     // this will also create a new cancellation token source.
                     await disconnect(false).ConfigureAwait(false);
 
+                    await Task.Delay(5000).ConfigureAwait(false);
+
                     // this token will be valid for the scope of this connection.
                     // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
                     var cancellationToken = connectCancelSource.Token;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 703444a..fb467472d3 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -22,6 +22,7 @@
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Graphics.Textures;
 using osu.Framework.Input;
+using osu.Framework.Input.Events;
 using osu.Framework.Input.Handlers;
 using osu.Framework.Input.Handlers.Joystick;
 using osu.Framework.Input.Handlers.Midi;
@@ -65,6 +66,7 @@
 using osu.Game.Scoring;
 using osu.Game.Skinning;
 using osu.Game.Utils;
+using osuTK.Input;
 using RuntimeInfo = osu.Framework.RuntimeInfo;
 
 namespace osu.Game
@@ -104,7 +106,7 @@ public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncPro
         /// </summary>
         private const double global_track_volume_adjust = 0.8;
 
-        public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
+        public virtual bool UseDevelopmentServer => false;
 
         public virtual EndpointConfiguration CreateEndpoints() =>
             UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
@@ -466,6 +468,20 @@ private void addFilesWarning()
             }
         }
 
+        protected override bool OnKeyDown(KeyDownEvent e)
+        {
+            if (e.Key == Key.F7)
+            {
+                Logger.Log("Forcing reconnect!", level: LogLevel.Important);
+
+                ((IStatefulUserHubClient)MultiplayerClient).ServerShuttingDown();
+                ((IStatefulUserHubClient)SpectatorClient).ServerShuttingDown();
+                ((IStatefulUserHubClient)metadataClient).ServerShuttingDown();
+            }
+
+            return base.OnKeyDown(e);
+        }
+
         private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track);
 
         protected virtual void InitialiseFonts()

```
…) — prioritize fork

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…BDSP 2-min sleep, MMAP StabilizedCallback skip, OsuGame BDSP factory

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4160543d-58b3-40e7-a293-c38a0b6fba4f

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…enQueue dup-handler fix, test type fix, mod utils)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/4160543d-58b3-40e7-a293-c38a0b6fba4f

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…r, SliderInputManager, DrawableSpinner, BeatmapCarousel, OsuGameAndroid, GameplaySampleTriggerSource, DrawableHitObject, oboe_bridge)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/49b81aac-c71a-4671-b650-5a258c6b1baf

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
@gitar-bot

gitar-bot Bot commented May 7, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Copilot AI and others added 3 commits May 7, 2026 17:02
…eSlider covariance, Catcher.computePositionInStack manual loop)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/60401bc7-7f04-4693-b4fb-40a5851d015a

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
- HitObjectContainer.AliveObjects: replace per-call new List + Sort with a
  dirty-flagged persistent cache. The list is rebuilt only when addDrawable /
  removeDrawable fires, eliminating allocations on every hit-policy check and
  every frame in LegacyCursorParticles/OsuModRelax (HIGH impact).

- DrawableHitCircle.DimmablePieces: cache the single-element Drawable[] as a
  field assigned in load() instead of allocating a new array on each property
  access from UpdateInitialTransforms / ClearNestedHitObjects (MEDIUM impact).

- DrawableOsuHitObject.ClearNestedHitObjects: replace DimmablePieces.OfType<>()
  LINQ iterator with a plain foreach + 'is DrawableHitObject' pattern-match,
  eliminating the LINQ state-machine allocation on every pool return (MEDIUM).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
…trictTracking/Bubbles hot paths

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/60401bc7-7f04-4693-b4fb-40a5851d015a

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
… touch/hit-policy/relax/taiko allocs)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/16daf401-e98b-42e6-8464-4745383d6e40

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 7, 2026 17:38
…edundant casts, unused usings

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/45232cd1-6884-4c60-815e-dba288232c8c

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 7, 2026 17:48
Copilot AI requested a review from winnerspiros May 7, 2026 18:21
Comment thread osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs Fixed
Comment thread osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs Fixed
Comment thread osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs Fixed
Comment thread osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs Fixed
Comment thread osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs Fixed
Comment thread osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs Fixed
…ource gen warning)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/e95ab934-869c-4165-ad22-563b0f51c6b8

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 8, 2026 04:36
… ISampleInfo[])

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/258720a5-84bc-4283-b524-809219539b6a

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 8, 2026 04:50
…ISampleInfo)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/6590546f-8ba3-432c-af4d-2128d82c40f4

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 8, 2026 05:08
…ectCode warning)

Agent-Logs-Url: https://github.com/winnerspiros/osu/sessions/3392b526-2e9e-4809-aa6c-cb6622e9295d

Co-authored-by: winnerspiros <1675249+winnerspiros@users.noreply.github.com>
Copilot AI requested a review from winnerspiros May 8, 2026 05:23
@winnerspiros
winnerspiros marked this pull request as ready for review May 8, 2026 05:43
Copilot AI review requested due to automatic review settings May 8, 2026 05:43
@winnerspiros
winnerspiros merged commit 065febf into master May 8, 2026
22 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR performs a second-pass performance audit across gameplay and platform hot paths, primarily reducing per-frame/per-judgement allocations (LINQ/state-machine removal) and adding a platform override point for background data processing.

Changes:

  • Replaces several LINQ queries in hot paths with manual loops / cached results (gameplay, scoring, UI, ruleset logic).
  • Adds a factory method for BackgroundDataStoreProcessor and provides an Android-specific override with a longer gameplay sleep interval.
  • Adjusts Android native audio stream opening to avoid StabilizedCallback overhead on MMAP paths, while keeping it for non-MMAP fallbacks.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
osu.Game/Screens/Select/BeatmapCarouselFilterSorting.cs Reworks aggregate max computation to a single-pass helper for hidden-beatmap filtering.
osu.Game/Screens/Play/ReplayPlayer.cs Caches autoplay detection to avoid repeated LINQ in fail-check path.
osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs Replaces FirstOrDefault LINQ with manual scan on judgement callback.
osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs Diff indicates event subscription adjustment in LoadComplete().
osu.Game/Rulesets/UI/HitObjectContainer.cs Introduces cached sorted alive-object list to avoid per-access allocations.
osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs Converts nested hitobject traversal to iterative scratch-list approach; modifies sample conversion in Play().
osu.Game/Rulesets/Scoring/ScoreProcessor.cs Caches applicable score mods array to avoid per-judgement LINQ allocation in rank updates.
osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs Avoids redundant sample array conversion by reusing a single converted array.
osu.Game/OsuGame.cs Adds CreateBackgroundDataStoreProcessor() override point and uses it during load.
osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs Adjusts test call to clear user mods using APIMod empty array.
osu.Game.Rulesets.Osu/UI/LegacyHitPolicy.cs Removes list copying in note-lock logic by single-pass enumeration.
osu.Game.Rulesets.Osu/Objects/Drawables/SliderInputManager.cs Removes OfType()/LastOrDefault() LINQ in slider nested-object handling.
osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs Removes LINQ from end-of-spinner and bonus tick selection paths.
osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs Removes LINQ from tick counting / any-hit checks; addresses formatting warning.
osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs Removes LINQ OfType() from dimmable-piece unsubscribe path.
osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs Caches DimmablePieces array to avoid per-access array allocation.
osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs Replaces OfType().First() with manual scan on tracking-change path.
osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs Removes per-frame OfType() LINQ in relax mod update loop.
osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs Replaces LINQ OfType().LastOrDefault() with reverse scan.
osu.Game.Rulesets.Catch/UI/Catcher.cs Removes LINQ Any() from stacking loop by extracting a manual predicate scan.
osu.Android/OsuGameAndroid.cs Caches ADPF input reporting interval; wires Android background processor override.
osu.Android/Native/oboe_bridge.cpp Two-pass stream open to avoid stabilization on MMAP; minor callback-side micro-optimisations.
osu.Android/AndroidBackgroundDataStoreProcessor.cs New Android-specific BackgroundDataStoreProcessor with extended gameplay sleep interval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +66
aliveObjectsSortedCache.Sort(static (a, b) => a.HitObject.StartTime.CompareTo(b.HitObject.StartTime));
aliveObjectsCacheDirty = false;
}

Comment on lines +153 to +168
/// <summary>
/// Returns the maximum value of <paramref name="func"/> over all non-hidden beatmaps in
/// <paramref name="beatmaps"/>, or <see cref="double.NegativeInfinity"/> if every beatmap
/// is hidden. Single-pass, allocation-free (no LINQ enumerator).
/// </summary>
private static double aggregateMax(IList<BeatmapInfo> beatmaps, Func<BeatmapInfo, double> func)
{
double max = double.NegativeInfinity;

foreach (var b in beatmaps)
{
if (b.Hidden) continue;

double v = func(b);
if (v > max) max = v;
}
Comment on lines +71 to 73
PlaySamples(nextObject.Samples.Cast<ISampleInfo>().ToArray());
}

Comment on lines 151 to 175
// Else we want the earliest valid nested.
// In cases of nested objects, they will always have earlier sample data than their parent object.
// Single-pass scan avoids the OrderBy + SkipWhile + FirstOrDefault LINQ chain.
// Iterative DFS with a shared scratch list avoids per-call state-machine allocations from recursive yield return.
double referenceTime = getReferenceTime();
HitObject? best = null;
double bestEnd = double.MaxValue;

foreach (var nested in getAllNested(mostValidObject.HitObject))
nestedScratch.Clear();
nestedScratch.AddRange(mostValidObject.HitObject.NestedHitObjects);

for (int i = 0; i < nestedScratch.Count; i++)
{
var nested = nestedScratch[i];
double end = nested.GetEndTime();

if (end > referenceTime && end < bestEnd)
{
best = nested;
bestEnd = end;
}

// Enqueue children for depth-first traversal.
if (nested.NestedHitObjects.Count > 0)
nestedScratch.AddRange(nested.NestedHitObjects);
}
Comment on lines +40 to +42
// AliveObjects already returns a new sorted List<T> from getSortedAliveObjects().
// Calling .ToList() on the IEnumerable<> would copy that list a second time.
// Single-pass over the enumerable uses only the one allocation that sorting requires.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants