Skip to content
Merged
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
35 changes: 5 additions & 30 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ jobs:
steps:
- name: Checkout project
uses: actions/checkout@v4

- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache dependencies
uses: Swatinem/rust-cache@v2

Expand All @@ -35,7 +35,7 @@ jobs:
steps:
- name: Checkout project
uses: actions/checkout@v4

- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable

Expand All @@ -58,10 +58,10 @@ jobs:
steps:
- name: Checkout project
uses: actions/checkout@v4

- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache dependencies
uses: Swatinem/rust-cache@v2

Expand All @@ -75,28 +75,3 @@ jobs:
--filter-expr 'test(util::sync::tests::share_gradual_taiko)'
--filter-expr 'test(taiko::difficulty::gradual::tests::next_and_nth)'
--no-fail-fast --failure-output=immediate-final

non_compact:
name: Test with raw_strains feature
runs-on: ubuntu-latest

steps:
- name: Checkout project
uses: actions/checkout@v4

- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache dependencies
uses: Swatinem/rust-cache@v2

- name: Install nextest
uses: taiki-e/install-action@nextest

- name: Run integration tests
run: >
cargo nextest run
--no-default-features
--features raw_strains
--test '*'
--no-fail-fast --failure-output=immediate-final
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ keywords = ["osu", "pp", "stars", "performance", "difficulty"]

[features]
default = []
raw_strains = []
sync = []
tracing = ["rosu-map/tracing"]

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ Calculating performances: Median: 40.57µs | Mean: 43.41µs
| Flag | Description | Dependencies
| ------------- | ------------------- | ------------
| `default` | No features enabled |
| `raw_strains` | With this feature, internal strain values will be stored in a plain `Vec`. This introduces an out-of-memory risk on maliciously long maps (see [/b/3739922](https://osu.ppy.sh/b/3739922)), but comes with a ~5% gain in performance. |
| `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This feature adds a small performance penalty. |
| `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]

Expand Down
25 changes: 11 additions & 14 deletions src/any/difficulty/skills.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::util::{float_ext::FloatExt, hint::unlikely, strains_vec::StrainsVec};
use crate::util::{
float_ext::FloatExt,
hint::unlikely,
traits::{IEnumerable, IOrderedEnumerable},
};

pub trait StrainSkill: Sized {
type DifficultyObject<'a>;
Expand Down Expand Up @@ -26,18 +30,15 @@ pub trait StrainSkill: Sized {
objects: &Self::DifficultyObjects<'a>,
);

fn into_current_strain_peaks(self) -> StrainsVec;
fn into_current_strain_peaks(self) -> Vec<f64>;

fn get_current_strain_peaks(
mut strain_peaks: StrainsVec,
current_section_peak: f64,
) -> StrainsVec {
fn get_current_strain_peaks(mut strain_peaks: Vec<f64>, current_section_peak: f64) -> Vec<f64> {
strain_peaks.push(current_section_peak);

strain_peaks
}

fn difficulty_value(current_strain_peaks: StrainsVec) -> f64;
fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64;

fn into_difficulty_value(self) -> f64;

Expand Down Expand Up @@ -80,21 +81,17 @@ pub fn count_top_weighted_strains(object_strains: &[f64], difficulty_value: f64)
.sum()
}

pub fn difficulty_value(current_strain_peaks: StrainsVec, decay_weight: f64) -> f64 {
pub fn difficulty_value(current_strain_peaks: Vec<f64>, decay_weight: f64) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;

// * 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.
let mut peaks = current_strain_peaks;
peaks.retain_non_zero_and_sort();

// SAFETY: we just removed all zeros
let peaks = unsafe { peaks.transmute_into_vec() };
let peaks = current_strain_peaks.cs_where(|&p| p > 0.0);

// * Difficulty is the weighted sum of the highest strains from every section.
// * We're sorting from highest to lowest strain.
for strain in peaks {
for strain in peaks.cs_order_descending() {
difficulty += strain * weight;
weight *= decay_weight;
}
Expand Down
2 changes: 1 addition & 1 deletion src/catch/strains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<CatchStrains, C
let DifficultyValues { movement, .. } = DifficultyValues::calculate(difficulty, &map);

Ok(CatchStrains {
movement: movement.into_current_strain_peaks().into_vec(),
movement: movement.into_current_strain_peaks(),
})
}
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@
//! | Flag | Description | Dependencies
//! | ------------- | ------------------- | ------------
//! | `default` | No features enabled |
//! | `raw_strains` | With this feature, internal strain values will be stored in a plain `Vec`. This introduces an out-of-memory risk on maliciously long maps (see [/b/3739922](https://osu.ppy.sh/b/3739922)), but comes with a ~5% gain in performance. |
//! | `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This feature adds a small performance penalty. |
//! | `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]
//!
Expand Down
2 changes: 1 addition & 1 deletion src/mania/strains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<ManiaStrains, C
let values = DifficultyValues::calculate(difficulty, &map);

Ok(ManiaStrains {
strains: values.strain.into_current_strain_peaks().into_vec(),
strains: values.strain.into_current_strain_peaks(),
})
}
6 changes: 3 additions & 3 deletions src/osu/difficulty/skills/aim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
skills::{StrainSkill, strain_decay},
},
osu::difficulty::{evaluators::AimEvaluator, object::OsuDifficultyObject},
util::{float_ext::FloatExt, strains_vec::StrainsVec},
util::float_ext::FloatExt,
};

use super::strain::OsuStrainSkill;
Expand All @@ -14,7 +14,7 @@ define_skill! {
pub struct Aim: StrainSkill => [OsuDifficultyObject<'a>][OsuDifficultyObject<'a>] {
include_sliders: bool,
current_strain: f64 = 0.0,
slider_strains: Vec<f64> = Vec::with_capacity(64), // TODO: use `StrainsVec`?
slider_strains: Vec<f64> = Vec::with_capacity(64),
}
}

Expand Down Expand Up @@ -75,7 +75,7 @@ impl Aim {

// From `OsuStrainSkill`; native rather than trait function so that it has
// priority over `StrainSkill::difficulty_value`
fn difficulty_value(current_strain_peaks: StrainsVec) -> f64 {
fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64 {
super::strain::difficulty_value(
current_strain_peaks,
Self::REDUCED_SECTION_COUNT,
Expand Down
6 changes: 3 additions & 3 deletions src/osu/difficulty/skills/flashlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
skills::strain_decay,
},
osu::difficulty::{evaluators::FlashlightEvaluator, object::OsuDifficultyObject},
util::strains_vec::StrainsVec,
util::traits::IEnumerable,
};

define_skill! {
Expand Down Expand Up @@ -61,8 +61,8 @@ impl Flashlight {
clippy::needless_pass_by_value,
reason = "function definition needs to stay in-sync with `StrainSkill::difficulty_value`"
)]
fn difficulty_value(current_strain_peaks: StrainsVec) -> f64 {
current_strain_peaks.sum()
fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64 {
current_strain_peaks.cs_sum()
}

pub fn difficulty_to_performance(difficulty: f64) -> f64 {
Expand Down
3 changes: 1 addition & 2 deletions src/osu/difficulty/skills/speed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::{
evaluators::{RhythmEvaluator, SpeedEvaluator},
object::OsuDifficultyObject,
},
util::strains_vec::StrainsVec,
};

use super::strain::OsuStrainSkill;
Expand Down Expand Up @@ -86,7 +85,7 @@ impl Speed {

// From `OsuStrainSkill`; native rather than trait function so that it has
// priority over `StrainSkill::difficulty_value`
fn difficulty_value(current_strain_peaks: StrainsVec) -> f64 {
fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64 {
super::strain::difficulty_value(
current_strain_peaks,
Self::REDUCED_SECTION_COUNT,
Expand Down
32 changes: 12 additions & 20 deletions src/osu/difficulty/skills/strain.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::util::{difficulty::logistic, float_ext::FloatExt, strains_vec::StrainsVec};
use crate::util::{
difficulty::logistic,
float_ext::FloatExt,
traits::{IEnumerable, IOrderedEnumerable},
};

pub trait OsuStrainSkill {
const REDUCED_SECTION_COUNT: usize = 10;
Expand All @@ -10,39 +14,27 @@ pub trait OsuStrainSkill {
}

pub fn difficulty_value(
current_strain_peaks: StrainsVec,
current_strain_peaks: Vec<f64>,
reduced_section_count: usize,
reduced_strain_baseline: f64,
decay_weight: f64,
) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;

let mut peaks = current_strain_peaks;
// * 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.
let peaks = current_strain_peaks.cs_where(|&p| p > 0.0);

// Note that we remove all initial zeros here.
let peaks_iter = peaks.sorted_non_zero_iter_mut().take(reduced_section_count);
let mut strains = peaks.cs_order_descending();

for (i, strain) in peaks_iter.enumerate() {
// Note that unless `reduced_strain_baseline == 0.0`, `strain` can
// never be `0.0`.
for (i, strain) in strains.iter_mut().take(reduced_section_count).enumerate() {
let clamped = f64::from((i as f32 / reduced_section_count as f32).clamp(0.0, 1.0));
let scale = f64::log10(lerp(1.0, 10.0, clamped));
*strain *= lerp(reduced_strain_baseline, 1.0, scale);
}

peaks.sort_desc();

// Sanity assert; will most definitely never panic
debug_assert!(reduced_strain_baseline != 0.0);

// SAFETY: As noted, zeros were removed from all initial strains and no
// strain was mutated to a zero afterwards.
let peaks = unsafe { peaks.transmute_into_vec() };

// Using `Vec<f64>` is much faster for iteration than `StrainsVec`

for strain in peaks {
for strain in strains.cs_order_descending() {
difficulty += strain * weight;
weight *= decay_weight;
}
Expand Down
8 changes: 4 additions & 4 deletions src/osu/strains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<OsuStrains, Con
} = DifficultyValues::calculate(difficulty, &map);

Ok(OsuStrains {
aim: aim.into_current_strain_peaks().into_vec(),
aim_no_sliders: aim_no_sliders.into_current_strain_peaks().into_vec(),
speed: speed.into_current_strain_peaks().into_vec(),
flashlight: flashlight.into_current_strain_peaks().into_vec(),
aim: aim.into_current_strain_peaks(),
aim_no_sliders: aim_no_sliders.into_current_strain_peaks(),
speed: speed.into_current_strain_peaks(),
flashlight: flashlight.into_current_strain_peaks(),
})
}
19 changes: 10 additions & 9 deletions src/taiko/difficulty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ use crate::{
},
object::TaikoObject,
},
util::difficulty::{norm, reverse_lerp},
util::{
difficulty::{norm, reverse_lerp},
traits::IOrderedEnumerable,
},
};

pub(crate) use self::skills::TaikoSkills;
Expand Down Expand Up @@ -111,11 +114,11 @@ fn combined_difficulty_value(
),
);

let mut peaks = combine_peaks(
rhythm_peaks.iter(),
reading_peaks.iter(),
color_peaks.iter(),
stamina_peaks.iter(),
let peaks = combine_peaks(
rhythm_peaks.iter().copied(),
reading_peaks.iter().copied(),
color_peaks.iter().copied(),
stamina_peaks.iter().copied(),
len,
is_relax,
is_convert,
Expand All @@ -130,9 +133,7 @@ fn combined_difficulty_value(
let mut difficulty = 0.0;
let mut weight = 1.0;

peaks.sort_by(|a, b| b.total_cmp(a));

for strain in peaks {
for strain in peaks.cs_order_descending() {
difficulty += strain * weight;
weight *= 0.9;
}
Expand Down
10 changes: 5 additions & 5 deletions src/taiko/strains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ pub fn strains(difficulty: &Difficulty, map: &Beatmap) -> Result<TaikoStrains, C
} = values.skills;

Ok(TaikoStrains {
color: color.into_current_strain_peaks().into_vec(),
reading: reading.into_current_strain_peaks().into_vec(),
rhythm: rhythm.into_current_strain_peaks().into_vec(),
stamina: stamina.into_current_strain_peaks().into_vec(),
single_color_stamina: single_color_stamina.into_current_strain_peaks().into_vec(),
color: color.into_current_strain_peaks(),
reading: reading.into_current_strain_peaks(),
rhythm: rhythm.into_current_strain_peaks(),
stamina: stamina.into_current_strain_peaks(),
single_color_stamina: single_color_stamina.into_current_strain_peaks(),
})
}
9 changes: 3 additions & 6 deletions src/util/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,7 @@ macro_rules! define_skill {
$( $fields )*
strain_skill_current_section_peak f64 = 0.0, // <-
strain_skill_current_section_end f64 = 0.0, // <-
strain_skill_strain_peaks crate::util::strains_vec::StrainsVec
= crate::util::strains_vec::StrainsVec::with_capacity(256), // <-
// TODO: use `StrainsVec`?
strain_skill_strain_peaks Vec<f64> = Vec::with_capacity(256), // <-
strain_skill_object_strains Vec<f64> = Vec::with_capacity(256), // <-
}
$( $rest )*
Expand Down Expand Up @@ -247,7 +245,6 @@ macro_rules! define_skill {
object::{IDifficultyObject, IDifficultyObjects, HasStartTime},
skills::{StrainSkill, StrainDecaySkill},
},
util::strains_vec::StrainsVec,
};

define_skill!( @impl $trait $name $objects[$object] );
Expand Down Expand Up @@ -316,14 +313,14 @@ macro_rules! define_skill {
= self.calculate_initial_strain(time, curr, objects);
}

fn into_current_strain_peaks(self) -> StrainsVec {
fn into_current_strain_peaks(self) -> Vec<f64> {
Self::get_current_strain_peaks(
self.strain_skill_strain_peaks,
self.strain_skill_current_section_peak,
)
}

fn difficulty_value(current_strain_peaks: StrainsVec) -> f64 {
fn difficulty_value(current_strain_peaks: Vec<f64>) -> f64 {
crate::any::difficulty::skills::difficulty_value(
current_strain_peaks,
Self::DECAY_WEIGHT,
Expand Down
2 changes: 1 addition & 1 deletion src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ pub mod map_or_attrs;
pub mod random;
pub mod ruleset_ext;
pub mod sort;
pub mod strains_vec;
pub mod sync;
pub mod traits;

#[macro_use]
mod macros;
Expand Down
Loading
Loading