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 4 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
23 changes: 19 additions & 4 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,26 @@ 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 maxCombo;
int hitCirclesCount, sliderCount, spinnerCount;

if (beatmap is ProgressiveCalculationBeatmap pcBeatmap)
{
maxCombo = pcBeatmap.GetMaxCombo();

hitCirclesCount = pcBeatmap.GetHitObjectCountOf(typeof(HitCircle));
sliderCount = pcBeatmap.GetHitObjectCountOf(typeof(Slider));
spinnerCount = pcBeatmap.GetHitObjectCountOf(typeof(Spinner));
}
Givikap120 marked this conversation as resolved.
Show resolved Hide resolved
else
{
maxCombo = beatmap.GetMaxCombo();

hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
sliderCount = beatmap.HitObjects.Count(h => h is Slider);
spinnerCount = beatmap.HitObjects.Count(h => h is 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
41 changes: 37 additions & 4 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 @@ -293,7 +294,7 @@
/// <summary>
/// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time.
/// </summary>
private class ProgressiveCalculationBeatmap : IBeatmap
protected class ProgressiveCalculationBeatmap : IBeatmap
{
private readonly IBeatmap baseBeatmap;

Expand All @@ -302,9 +303,41 @@
this.baseBeatmap = baseBeatmap;
}

public readonly List<HitObject> HitObjects = new List<HitObject>();
public void AddHitObject(HitObject hitObject)
{
hitObjects.Add(hitObject);

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

Check notice on line 311 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 311
hitObjectsCounts[objectType] = 0; // Initialize to 0 if not present
hitObjectsCounts[objectType]++;
}

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;

IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
private int comboObjectIndex, combo;

public int GetMaxCombo()
{
for (; comboObjectIndex < hitObjects.Count; comboObjectIndex++)
addCombo(hitObjects[comboObjectIndex], 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);
}
Givikap120 marked this conversation as resolved.
Show resolved Hide resolved
}

#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);
}
}
}
Loading