Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timed Difficulty Attributes calculation optimization #29482

Closed
wants to merge 6 commits into from
Closed
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
7 changes: 4 additions & 3 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat

double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;

int maxCombo = beatmap.GetMaxCombo();

int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
int hitCirclesCount = beatmap.GetHitObjectCountOf(typeof(HitCircle));
int sliderCount = beatmap.GetHitObjectCountOf(typeof(Slider));
int spinnerCount = beatmap.GetHitObjectCountOf(typeof(Spinner));

HitWindows hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
Expand Down
22 changes: 13 additions & 9 deletions osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.Collections.Generic;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Framework.Utils;

namespace osu.Game.Rulesets.Osu.Difficulty.Skills
Expand Down Expand Up @@ -44,22 +43,27 @@ public override double DifficultyValue()
double difficulty = 0;
double weight = 1;

// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
List<double> strains = GetCurrentStrainsSorted();

List<double> strains = peaks.OrderDescending().ToList();
int reducedSectionCount = Math.Min(strains.Count, ReducedSectionCount);
double[] reducedStrains = new double[reducedSectionCount];

// We are reducing the highest strains first to account for extreme difficulty spikes
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
for (int i = 0; i < reducedSectionCount; i++)
{
double scale = Math.Log10(Interpolation.Lerp(1, 10, Math.Clamp((float)i / ReducedSectionCount, 0, 1)));
strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
reducedStrains[i] = strains[i] * Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale);
}

// Remove reduced strains as they are no longer sorted
strains.RemoveRange(0, reducedSectionCount);

// Insert them back
foreach (double reducedStrain in reducedStrains)
InsertElementInReverseSortedList(strains, reducedStrain);

// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderDescending())
foreach (double strain in strains)
{
difficulty += strain * weight;
weight *= DecayWeight;
Expand Down
20 changes: 20 additions & 0 deletions osu.Game/Beatmaps/Beatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Newtonsoft.Json;
using osu.Framework.Lists;
using osu.Game.IO.Serialization.Converters;
using osu.Game.Rulesets.Scoring;

namespace osu.Game.Beatmaps
{
Expand Down Expand Up @@ -115,6 +116,25 @@ public double GetMostCommonBeatLength()
return mostCommon.beatLength;
}

public int GetMaxCombo()
{
int combo = 0;
foreach (var h in HitObjects)
addCombo(h, ref combo);
return combo;

static void addCombo(HitObject hitObject, ref int combo)
{
if (hitObject.Judgement.MaxResult.AffectsCombo())
combo++;

foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested, ref combo);
}
}

public int GetHitObjectCountOf(Type type) => HitObjects.Count(h => h.GetType() == type);

IBeatmap IBeatmap.Clone() => Clone();

public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
Expand Down
31 changes: 10 additions & 21 deletions osu.Game/Beatmaps/IBeatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;

namespace osu.Game.Beatmaps
{
Expand Down Expand Up @@ -74,6 +73,16 @@ public interface IBeatmap
/// </summary>
/// <returns>The shallow-cloned beatmap.</returns>
IBeatmap Clone();

/// <summary>
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
/// </summary>
int GetMaxCombo();

/// <summary>
/// Finds amount of <see cref="HitObject"/>s that have given type.
/// </summary>
int GetHitObjectCountOf(Type type);
}

/// <summary>
Expand All @@ -90,26 +99,6 @@ public interface IBeatmap<out T> : IBeatmap

public static class BeatmapExtensions
{
/// <summary>
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
/// </summary>
public static int GetMaxCombo(this IBeatmap beatmap)
{
int combo = 0;
foreach (var h in beatmap.HitObjects)
addCombo(h, ref combo);
return combo;

static void addCombo(HitObject hitObject, ref int combo)
{
if (hitObject.Judgement.MaxResult.AffectsCombo())
combo++;

foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested, ref combo);
}
}

/// <summary>
/// Find the total milliseconds between the first and last hittable objects.
/// </summary>
Expand Down
36 changes: 33 additions & 3 deletions osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Utils;

namespace osu.Game.Rulesets.Difficulty
Expand Down Expand Up @@ -114,7 +115,7 @@

foreach (var obj in Beatmap.HitObjects)
{
progressiveBeatmap.HitObjects.Add(obj);
progressiveBeatmap.AddHitObject(obj);

while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime())
{
Expand Down Expand Up @@ -302,9 +303,38 @@
this.baseBeatmap = baseBeatmap;
}

public readonly List<HitObject> HitObjects = new List<HitObject>();
private int maxCombo;

IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
public int GetMaxCombo() => maxCombo;

public void AddHitObject(HitObject hitObject)
{
hitObjects.Add(hitObject);

var objectType = hitObject.GetType();
if (!hitObjectsCounts.ContainsKey(objectType))

Check notice on line 315 in osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Dictionary lookup can be simplified with 'TryAdd' in osu.Game\Rulesets\Difficulty\DifficultyCalculator.cs on line 315
hitObjectsCounts[objectType] = 0; // Initialize to 0 if not present
hitObjectsCounts[objectType]++;

addCombo(hitObject);
}

private void addCombo(HitObject hitObject)
{
if (hitObject.Judgement.MaxResult.AffectsCombo())
maxCombo++;

foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested);
}

private readonly List<HitObject> hitObjects = new List<HitObject>();

private readonly Dictionary<Type, int> hitObjectsCounts = new Dictionary<Type, int>();

public int GetHitObjectCountOf(Type type) => hitObjectsCounts.GetValueOrDefault(type);

IReadOnlyList<HitObject> IBeatmap.HitObjects => hitObjects;

#region Delegated IBeatmap implementation

Expand Down
85 changes: 79 additions & 6 deletions osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,17 @@ public sealed override void Process(DifficultyHitObject current)
saveCurrentPeak();
startNewSectionFrom(currentSectionEnd, current);
currentSectionEnd += SectionLength;

amountOfStrainsAddedSinceSave++;
}

currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak);
double currentStrain = StrainValueAt(current);

if (currentSectionPeak < currentStrain)
{
currentSectionPeak = currentStrain;
isSavedCurrentStrainRelevant = false;
}
}

/// <summary>
Expand Down Expand Up @@ -102,19 +110,84 @@ public override double DifficultyValue()
double difficulty = 0;
double weight = 1;

// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);

// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in peaks.OrderDescending())
foreach (double strain in GetCurrentStrainsSorted())
{
difficulty += strain * weight;
weight *= DecayWeight;
}

return difficulty;
}

protected List<double> GetCurrentStrainsSorted()
{
List<double> strains;

// If no saved strains - calculate them from 0, and save them after that
if (savedSortedStrains == null || savedSortedStrains.Count == 0)
{
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);

strains = peaks.OrderDescending().ToList();

savedSortedStrains = new List<double>(strains);
amountOfStrainsAddedSinceSave = 0;
savedCurrentStrain = currentSectionPeak;
isSavedCurrentStrainRelevant = true;
}
// If several sections were added since last save - insert them into saved strains list
else if (amountOfStrainsAddedSinceSave > 0)
{
var newPeaks = GetCurrentStrainPeaks().TakeLast(amountOfStrainsAddedSinceSave).Where(p => p > 0);
foreach (double newPeak in newPeaks)
InsertElementInReverseSortedList(savedSortedStrains, newPeak);

strains = new List<double>(savedSortedStrains);

amountOfStrainsAddedSinceSave = 0;
savedCurrentStrain = currentSectionPeak;
isSavedCurrentStrainRelevant = true;
}
// If no section was added, but last one was changed - find it and replace it with new one
else if (!isSavedCurrentStrainRelevant && savedCurrentStrain > 0)
{
int invalidStrainIndex = savedSortedStrains.BinarySearch(savedCurrentStrain, new ReverseComparer());
savedSortedStrains.RemoveAt(invalidStrainIndex);
InsertElementInReverseSortedList(savedSortedStrains, currentSectionPeak);

strains = new List<double>(savedSortedStrains);

savedCurrentStrain = currentSectionPeak;
isSavedCurrentStrainRelevant = true;
}
// Otherwise - just use saved strains
else
{
strains = new List<double>(savedSortedStrains);
}

return strains;
}

private List<double>? savedSortedStrains;
private double savedCurrentStrain;
private bool isSavedCurrentStrainRelevant;
private int amountOfStrainsAddedSinceSave;

protected static void InsertElementInReverseSortedList(List<double> list, double element)
{
int indexToInsert = list.BinarySearch(element, new ReverseComparer());
if (indexToInsert < 0)
indexToInsert = ~indexToInsert;

list.Insert(indexToInsert, element);
}
Comment on lines +179 to +186
Copy link
Collaborator

Choose a reason for hiding this comment

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

Before I go through all of the changes in this file (which, is a lot of code added, just for this use case) - why are things like this being reinvented here? Can you not just use SortedList or something?

I have a feeling that if you try SortedList or another proper data structure a lot of the code above might just disappear.

Copy link
Contributor Author

@Givikap120 Givikap120 Aug 19, 2024

Choose a reason for hiding this comment

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

Using normal List I have full control over what and when is being sorted. SortedList is a dictionary with unique keys, what means that it's unsuitable for this task, as strains can have the same value. Some time ago I tried to do it with SortedSet (what still have the same "uniqueness" issue) and it was slower than normal way.

@tsunyoku probably can give better explanation on this as we discussed this problem with him


private class ReverseComparer : IComparer<double>
{
public int Compare(double x, double y) => Comparer<double>.Default.Compare(y, x);
}
}
}
4 changes: 4 additions & 0 deletions osu.Game/Screens/Edit/EditorBeatmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ SortedList<BreakPeriod> IBeatmap.Breaks

public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength();

public int GetMaxCombo() => PlayableBeatmap.GetMaxCombo();

public int GetHitObjectCountOf(Type type) => PlayableBeatmap.GetHitObjectCountOf(type);

public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();

private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
Expand Down
Loading