feat: compact strains (#32)
* store zero strains compactly * added docs * added proptest * add feature to store strains compactly * update ci * rename CompactZerosVec to StrainsVec * add size_hint for iter * rename iter * minor adjustments * more minor adjustments
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
name: Clippy and Tests
|
name: Clippy checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout project
|
- name: Checkout project
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -29,34 +29,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cargo install cargo-rdme
|
cargo install cargo-rdme
|
||||||
cargo rdme --check --no-fail-on-warnings
|
cargo rdme --check --no-fail-on-warnings
|
||||||
|
|
||||||
tests:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout project
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- 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 tests on default features
|
|
||||||
run: cargo nextest run --no-default-features --no-fail-fast --failure-output=immediate-final
|
|
||||||
|
|
||||||
- name: Run tests with sync feature
|
|
||||||
run: >
|
|
||||||
cargo nextest run
|
|
||||||
--features sync
|
|
||||||
--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
|
|
||||||
|
|
||||||
- name: Run doctests
|
|
||||||
run: cargo test --no-default-features --doc
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- next
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
doc:
|
||||||
|
name: Doc tests
|
||||||
|
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: Run doctests
|
||||||
|
run: >
|
||||||
|
cargo test
|
||||||
|
--doc
|
||||||
|
--no-default-features --features compact_strains
|
||||||
|
|
||||||
|
default:
|
||||||
|
name: Default tests
|
||||||
|
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 all tests
|
||||||
|
run: >
|
||||||
|
cargo nextest run
|
||||||
|
--no-default-features --features compact_strains
|
||||||
|
--no-fail-fast --failure-output=immediate-final
|
||||||
|
|
||||||
|
sync:
|
||||||
|
name: Test sync 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 specific tests
|
||||||
|
run: >
|
||||||
|
cargo nextest run
|
||||||
|
--features sync
|
||||||
|
--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 without compact_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
|
||||||
|
--test '*'
|
||||||
|
--no-fail-fast --failure-output=immediate-final
|
||||||
+2
-1
@@ -11,7 +11,8 @@ description = "Difficulty and performance calculation for osu!"
|
|||||||
keywords = ["osu", "pp", "stars", "performance", "osu!"]
|
keywords = ["osu", "pp", "stars", "performance", "osu!"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["compact_strains"]
|
||||||
|
compact_strains = []
|
||||||
sync = []
|
sync = []
|
||||||
tracing = ["rosu-map/tracing"]
|
tracing = ["rosu-map/tracing"]
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,12 @@ Calculating performances: Median: 44.13µs | Mean: 45.53µs
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
| Flag | Description | Dependencies
|
| Flag | Description | Dependencies
|
||||||
| --------- | ----------- | ------------
|
| ----------------- | ------------------------------------- | ------------
|
||||||
| `default` | No features |
|
| `default` | Enables the `compact_strains` feature |
|
||||||
| `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This adds a performance penalty so only enable this if really needed. |
|
| `compact_strains` | Storing internal strain values in a plain Vec introduces an out-of-memory risk on maliciously long maps (see [/b/3739922](https://osu.ppy.sh/b/3739922)). This feature stores strains more compactly, but comes with a ~5% loss in performance. |
|
||||||
| `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]
|
| `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This adds a performance penalty so only enable this if really needed. |
|
||||||
|
| `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]
|
||||||
|
|
||||||
### Bindings
|
### Bindings
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use crate::util::strains_vec::StrainsVec;
|
||||||
|
|
||||||
pub fn strain_decay(ms: f64, strain_decay_base: f64) -> f64 {
|
pub fn strain_decay(ms: f64, strain_decay_base: f64) -> f64 {
|
||||||
strain_decay_base.powf(ms / 1000.0)
|
strain_decay_base.powf(ms / 1000.0)
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,7 @@ pub trait ISkill {
|
|||||||
pub struct StrainSkill {
|
pub struct StrainSkill {
|
||||||
pub curr_section_peak: f64,
|
pub curr_section_peak: f64,
|
||||||
pub curr_section_end: f64,
|
pub curr_section_end: f64,
|
||||||
pub strain_peaks: Vec<f64>,
|
pub strain_peaks: StrainsVec,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StrainSkill {
|
impl Default for StrainSkill {
|
||||||
@@ -36,7 +38,7 @@ impl Default for StrainSkill {
|
|||||||
curr_section_peak: 0.0,
|
curr_section_peak: 0.0,
|
||||||
curr_section_end: 0.0,
|
curr_section_end: 0.0,
|
||||||
// mean=386.81 | median=279
|
// mean=386.81 | median=279
|
||||||
strain_peaks: Vec::with_capacity(256),
|
strain_peaks: StrainsVec::with_capacity(256),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +55,7 @@ impl StrainSkill {
|
|||||||
self.curr_section_peak = initial_strain;
|
self.curr_section_peak = initial_strain;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
let mut strain_peaks = self.strain_peaks;
|
let mut strain_peaks = self.strain_peaks;
|
||||||
strain_peaks.push(self.curr_section_peak);
|
strain_peaks.push(self.curr_section_peak);
|
||||||
|
|
||||||
@@ -65,10 +67,8 @@ impl StrainSkill {
|
|||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
let mut peaks = self.get_curr_strain_peaks();
|
let mut peaks = self.get_curr_strain_peaks();
|
||||||
peaks.retain(|&strain| strain > 0.0);
|
|
||||||
peaks.sort_by(|a, b| b.total_cmp(a));
|
|
||||||
|
|
||||||
for strain in peaks {
|
for strain in peaks.sorted_non_zero_iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= decay_weight;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ impl StrainDecaySkill {
|
|||||||
self.inner.start_new_section_from(initial_strain);
|
self.inner.start_new_section_from(initial_strain);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
skills::{strain_decay, ISkill, Skill, StrainDecaySkill},
|
skills::{strain_decay, ISkill, Skill, StrainDecaySkill},
|
||||||
},
|
},
|
||||||
catch::difficulty::object::CatchDifficultyObject,
|
catch::difficulty::object::CatchDifficultyObject,
|
||||||
|
util::strains_vec::StrainsVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
||||||
@@ -107,7 +108,7 @@ impl Movement {
|
|||||||
dist_addition / weighted_strain_time
|
dist_addition / weighted_strain_time
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ pub fn strains(difficulty: &Difficulty, converted: &CatchBeatmap<'_>) -> CatchSt
|
|||||||
let DifficultyValues { movement, .. } = DifficultyValues::calculate(difficulty, converted);
|
let DifficultyValues { movement, .. } = DifficultyValues::calculate(difficulty, converted);
|
||||||
|
|
||||||
CatchStrains {
|
CatchStrains {
|
||||||
movement: movement.get_curr_strain_peaks(),
|
movement: movement.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -121,11 +121,12 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
//! | Flag | Description | Dependencies
|
//! | Flag | Description | Dependencies
|
||||||
//! | --------- | ----------- | ------------
|
//! | ----------------- | ------------------------------------- | ------------
|
||||||
//! | `default` | No features |
|
//! | `default` | Enables the `compact_strains` feature |
|
||||||
//! | `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This adds a performance penalty so only enable this if really needed. |
|
//! | `compact_strains` | Storing internal strain values in a plain Vec introduces an out-of-memory risk on maliciously long maps (see [/b/3739922](https://osu.ppy.sh/b/3739922)). This feature stores strains more compactly, but comes with a ~5% loss in performance. |
|
||||||
//! | `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]
|
//! | `sync` | Some gradual calculation types can only be shared across threads if this feature is enabled. This adds a performance penalty so only enable this if really needed. |
|
||||||
|
//! | `tracing` | Any error encountered during beatmap decoding will be logged through `tracing::error`. If this feature is **not** enabled, errors will be ignored. | [`tracing`]
|
||||||
//!
|
//!
|
||||||
//! ## Bindings
|
//! ## Bindings
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::{
|
|||||||
skills::{strain_decay, ISkill, Skill, StrainDecaySkill},
|
skills::{strain_decay, ISkill, Skill, StrainDecaySkill},
|
||||||
},
|
},
|
||||||
mania::difficulty::object::ManiaDifficultyObject,
|
mania::difficulty::object::ManiaDifficultyObject,
|
||||||
|
util::strains_vec::StrainsVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INDIVIDUAL_DECAY_BASE: f64 = 0.125;
|
const INDIVIDUAL_DECAY_BASE: f64 = 0.125;
|
||||||
@@ -37,7 +38,7 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ pub fn strains(difficulty: &Difficulty, converted: &ManiaBeatmap<'_>) -> ManiaSt
|
|||||||
let values = DifficultyValues::calculate(difficulty, converted);
|
let values = DifficultyValues::calculate(difficulty, converted);
|
||||||
|
|
||||||
ManiaStrains {
|
ManiaStrains {
|
||||||
strains: values.strain.get_curr_strain_peaks(),
|
strains: values.strain.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
skills::{strain_decay, ISkill, Skill},
|
skills::{strain_decay, ISkill, Skill},
|
||||||
},
|
},
|
||||||
osu::difficulty::object::OsuDifficultyObject,
|
osu::difficulty::object::OsuDifficultyObject,
|
||||||
util::float_ext::FloatExt,
|
util::{float_ext::FloatExt, strains_vec::StrainsVec},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::strain::OsuStrainSkill;
|
use super::strain::OsuStrainSkill;
|
||||||
@@ -30,7 +30,7 @@ impl Aim {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
skills::{strain_decay, ISkill, Skill, StrainSkill},
|
skills::{strain_decay, ISkill, Skill, StrainSkill},
|
||||||
},
|
},
|
||||||
osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind},
|
osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind},
|
||||||
util::mods::Mods,
|
util::{mods::Mods, strains_vec::StrainsVec},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::strain::OsuStrainSkill;
|
use super::strain::OsuStrainSkill;
|
||||||
@@ -33,7 +33,7 @@ impl Flashlight {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +48,7 @@ impl Flashlight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn static_difficulty_value(skill: StrainSkill) -> f64 {
|
fn static_difficulty_value(skill: StrainSkill) -> f64 {
|
||||||
skill.get_curr_strain_peaks().into_iter().sum::<f64>()
|
skill.get_curr_strain_peaks().sum() * OsuStrainSkill::DIFFICULTY_MULTIPLER
|
||||||
* OsuStrainSkill::DIFFICULTY_MULTIPLER
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{
|
|||||||
skills::{strain_decay, ISkill, Skill},
|
skills::{strain_decay, ISkill, Skill},
|
||||||
},
|
},
|
||||||
osu::difficulty::object::OsuDifficultyObject,
|
osu::difficulty::object::OsuDifficultyObject,
|
||||||
|
util::strains_vec::StrainsVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::strain::OsuStrainSkill;
|
use super::strain::OsuStrainSkill;
|
||||||
@@ -37,7 +38,7 @@ impl Speed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::any::difficulty::skills::StrainSkill;
|
use crate::{any::difficulty::skills::StrainSkill, util::strains_vec::StrainsVec};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct OsuStrainSkill {
|
pub struct OsuStrainSkill {
|
||||||
@@ -21,7 +21,7 @@ impl OsuStrainSkill {
|
|||||||
self.inner.start_new_section_from(initial_strain);
|
self.inner.start_new_section_from(initial_strain);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +36,8 @@ impl OsuStrainSkill {
|
|||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
let mut peaks = self.get_curr_strain_peaks();
|
let mut peaks = self.get_curr_strain_peaks();
|
||||||
peaks.retain(|&strain| strain > 0.0);
|
|
||||||
peaks.sort_by(|a, b| b.total_cmp(a));
|
|
||||||
|
|
||||||
let peaks_iter = peaks.iter_mut().take(reduced_section_count);
|
let peaks_iter = peaks.sorted_non_zero_iter_mut().take(reduced_section_count);
|
||||||
|
|
||||||
for (i, strain) in peaks_iter.enumerate() {
|
for (i, strain) in peaks_iter.enumerate() {
|
||||||
let clamped = f64::from((i as f32 / reduced_section_count as f32).clamp(0.0, 1.0));
|
let clamped = f64::from((i as f32 / reduced_section_count as f32).clamp(0.0, 1.0));
|
||||||
@@ -47,9 +45,9 @@ impl OsuStrainSkill {
|
|||||||
*strain *= lerp(reduced_strain_baseline, 1.0, scale);
|
*strain *= lerp(reduced_strain_baseline, 1.0, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
peaks.sort_by(|a, b| b.total_cmp(a));
|
peaks.sort_desc();
|
||||||
|
|
||||||
for strain in peaks {
|
for strain in peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= decay_weight;
|
weight *= decay_weight;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -38,9 +38,9 @@ pub fn strains(difficulty: &Difficulty, converted: &OsuBeatmap<'_>) -> OsuStrain
|
|||||||
} = DifficultyValues::calculate(difficulty, converted);
|
} = DifficultyValues::calculate(difficulty, converted);
|
||||||
|
|
||||||
OsuStrains {
|
OsuStrains {
|
||||||
aim: aim.get_curr_strain_peaks(),
|
aim: aim.get_curr_strain_peaks().into_vec(),
|
||||||
aim_no_sliders: aim_no_sliders.get_curr_strain_peaks(),
|
aim_no_sliders: aim_no_sliders.get_curr_strain_peaks().into_vec(),
|
||||||
speed: speed.get_curr_strain_peaks(),
|
speed: speed.get_curr_strain_peaks().into_vec(),
|
||||||
flashlight: flashlight.get_curr_strain_peaks(),
|
flashlight: flashlight.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
object::{TaikoDifficultyObject, TaikoDifficultyObjects},
|
object::{TaikoDifficultyObject, TaikoDifficultyObjects},
|
||||||
},
|
},
|
||||||
util::sync::{RefCount, Weak},
|
util::{
|
||||||
|
strains_vec::StrainsVec,
|
||||||
|
sync::{RefCount, Weak},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 0.12;
|
const SKILL_MULTIPLIER: f64 = 0.12;
|
||||||
@@ -43,7 +46,7 @@ impl Color {
|
|||||||
ColorEvaluator::evaluate_diff_of(curr)
|
ColorEvaluator::evaluate_diff_of(curr)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,8 @@ impl Peaks {
|
|||||||
|
|
||||||
let zip = color_peaks
|
let zip = color_peaks
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.zip(rhythm_peaks.iter())
|
||||||
.zip(rhythm_peaks.iter().copied())
|
.zip(stamina_peaks.iter());
|
||||||
.zip(stamina_peaks.iter().copied());
|
|
||||||
|
|
||||||
for ((mut color_peak, mut rhythm_peak), mut stamina_peak) in zip {
|
for ((mut color_peak, mut rhythm_peak), mut stamina_peak) in zip {
|
||||||
color_peak *= COLOR_SKILL_MULTIPLIER;
|
color_peak *= COLOR_SKILL_MULTIPLIER;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
object::HitType,
|
object::HitType,
|
||||||
},
|
},
|
||||||
util::{float_ext::FloatExt, limited_queue::LimitedQueue},
|
util::{float_ext::FloatExt, limited_queue::LimitedQueue, strains_vec::StrainsVec},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 10.0;
|
const SKILL_MULTIPLIER: f64 = 10.0;
|
||||||
@@ -142,7 +142,7 @@ impl Rhythm {
|
|||||||
self.curr_strain()
|
self.curr_strain()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::{
|
|||||||
difficulty::object::{TaikoDifficultyObject, TaikoDifficultyObjects},
|
difficulty::object::{TaikoDifficultyObject, TaikoDifficultyObjects},
|
||||||
object::HitType,
|
object::HitType,
|
||||||
},
|
},
|
||||||
|
util::strains_vec::StrainsVec,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f64 = 1.1;
|
const SKILL_MULTIPLIER: f64 = 1.1;
|
||||||
@@ -18,7 +19,7 @@ pub struct Stamina {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Stamina {
|
impl Stamina {
|
||||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||||
self.inner.get_curr_strain_peaks()
|
self.inner.get_curr_strain_peaks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ pub fn strains(difficulty: &Difficulty, converted: &TaikoBeatmap<'_>) -> TaikoSt
|
|||||||
let values = DifficultyValues::calculate(difficulty, converted);
|
let values = DifficultyValues::calculate(difficulty, converted);
|
||||||
|
|
||||||
TaikoStrains {
|
TaikoStrains {
|
||||||
color: values.peaks.color.get_curr_strain_peaks(),
|
color: values.peaks.color.get_curr_strain_peaks().into_vec(),
|
||||||
rhythm: values.peaks.rhythm.get_curr_strain_peaks(),
|
rhythm: values.peaks.rhythm.get_curr_strain_peaks().into_vec(),
|
||||||
stamina: values.peaks.stamina.get_curr_strain_peaks(),
|
stamina: values.peaks.stamina.get_curr_strain_peaks().into_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ pub mod map_or_attrs;
|
|||||||
pub mod mods;
|
pub mod mods;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
pub mod sort;
|
pub mod sort;
|
||||||
|
pub mod strains_vec;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
pub use inner::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "compact_strains")]
|
||||||
|
mod inner {
|
||||||
|
use std::{iter::Copied, slice::Iter};
|
||||||
|
|
||||||
|
use self::entry::StrainsEntry;
|
||||||
|
|
||||||
|
/// A specialized `Vec<f64>` where all entries must be non-negative.
|
||||||
|
///
|
||||||
|
/// It is compact in the sense that zeros are not stored directly but instead
|
||||||
|
/// as amount of times they appear consecutively.
|
||||||
|
///
|
||||||
|
/// For cases with few consecutive zeros, this type generally reduces
|
||||||
|
/// performance slightly. However, for edge cases like `/b/3739922` the length
|
||||||
|
/// of the list is massively reduced, preventing out-of-memory issues.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StrainsVec {
|
||||||
|
inner: Vec<StrainsEntry>,
|
||||||
|
len: usize,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
// Ensures that methods are used correctly
|
||||||
|
has_zero: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainsVec {
|
||||||
|
/// Constructs a new, empty [`StrainsVec`] with at least the specified
|
||||||
|
/// capacity.
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Vec::with_capacity(capacity),
|
||||||
|
len: 0,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
has_zero: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of elements.
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends an element to the back.
|
||||||
|
pub fn push(&mut self, value: f64) {
|
||||||
|
if value.to_bits() > 0 {
|
||||||
|
self.inner.push(StrainsEntry::new_value(value));
|
||||||
|
} else if let Some(last) = self.inner.last_mut().filter(|e| e.is_zero()) {
|
||||||
|
last.incr_zero_count();
|
||||||
|
} else {
|
||||||
|
self.inner.push(StrainsEntry::new_zero());
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
self.has_zero = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sorts the entries in descending order.
|
||||||
|
pub fn sort_desc(&mut self) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug_assert!(!self.has_zero);
|
||||||
|
|
||||||
|
self.inner.sort_by(|a, b| b.value().total_cmp(&a.value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all zero entries
|
||||||
|
pub fn retain_non_zero(&mut self) {
|
||||||
|
self.inner.retain(StrainsEntry::is_value);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
self.has_zero = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all zeros and sorts the remaining entries in descending order.
|
||||||
|
pub fn retain_non_zero_and_sort(&mut self) {
|
||||||
|
self.retain_non_zero();
|
||||||
|
self.sort_desc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over the raw entries, assuming that there are no zeros.
|
||||||
|
///
|
||||||
|
/// Panics if there are zeros.
|
||||||
|
pub fn non_zero_iter(&self) -> impl ExactSizeIterator<Item = f64> + '_ {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug_assert!(!self.has_zero);
|
||||||
|
|
||||||
|
self.inner.iter().copied().map(StrainsEntry::value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [`StrainsVec::retain_non_zero_and_sort`] followed by
|
||||||
|
/// [`StrainsVec::iter`] but the resulting iterator is faster
|
||||||
|
/// because it doesn't need to check whether entries are zero.
|
||||||
|
pub fn sorted_non_zero_iter(&mut self) -> impl ExactSizeIterator<Item = f64> + '_ {
|
||||||
|
self.retain_non_zero_and_sort();
|
||||||
|
|
||||||
|
self.non_zero_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all zeros, sorts the remaining entries in descending order, and
|
||||||
|
/// returns an iterator over mutable references to the values.
|
||||||
|
pub fn sorted_non_zero_iter_mut(&mut self) -> impl ExactSizeIterator<Item = &mut f64> {
|
||||||
|
self.retain_non_zero_and_sort();
|
||||||
|
|
||||||
|
self.inner.iter_mut().map(StrainsEntry::as_value_mut)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sum up all values.
|
||||||
|
pub fn sum(&self) -> f64 {
|
||||||
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(StrainsEntry::is_value)
|
||||||
|
.fold(0.0, |sum, e| sum + e.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the [`StrainsVec`].
|
||||||
|
pub fn iter(&self) -> StrainsIter<'_> {
|
||||||
|
StrainsIter::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates a new `Vec<f64>` to store all values, including zeros.
|
||||||
|
pub fn into_vec(self) -> Vec<f64> {
|
||||||
|
let mut vec = Vec::with_capacity(self.len);
|
||||||
|
vec.extend(self.iter());
|
||||||
|
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StrainsIter<'a> {
|
||||||
|
inner: Copied<Iter<'a, StrainsEntry>>,
|
||||||
|
curr: Option<StrainsEntry>,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> StrainsIter<'a> {
|
||||||
|
pub fn new(vec: &'a StrainsVec) -> Self {
|
||||||
|
let mut inner = vec.inner.iter().copied();
|
||||||
|
let curr = inner.next();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
curr,
|
||||||
|
len: vec.len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for StrainsIter<'a> {
|
||||||
|
type Item = f64;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
loop {
|
||||||
|
let curr = self.curr.as_mut()?;
|
||||||
|
|
||||||
|
if curr.is_value() {
|
||||||
|
let value = curr.value();
|
||||||
|
self.curr = self.inner.next();
|
||||||
|
self.len -= 1;
|
||||||
|
|
||||||
|
return Some(value);
|
||||||
|
} else if curr.zero_count() > 0 {
|
||||||
|
curr.decr_zero_count();
|
||||||
|
self.len -= 1;
|
||||||
|
|
||||||
|
return Some(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.curr = self.inner.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let len = self.len();
|
||||||
|
|
||||||
|
(len, Some(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for StrainsIter<'_> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private module to hide internal fields.
|
||||||
|
mod entry {
|
||||||
|
/// Either a positive `f64` or an amount of consecutive `0.0`.
|
||||||
|
///
|
||||||
|
/// If the first bit is not set, i.e. the sign bit of a `f64` indicates
|
||||||
|
/// that it's positive, the union represents that `f64`. Otherwise, the
|
||||||
|
/// first bit is ignored and the union represents a `u64`.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub union StrainsEntry {
|
||||||
|
value: f64,
|
||||||
|
zero_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainsEntry {
|
||||||
|
const ZERO_COUNT_MASK: u64 = u64::MAX >> 1;
|
||||||
|
|
||||||
|
pub fn new_value(value: f64) -> Self {
|
||||||
|
debug_assert!(
|
||||||
|
value.is_sign_positive(),
|
||||||
|
"attempted to create negative strain entry, please report as a bug"
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new_zero() -> Self {
|
||||||
|
Self {
|
||||||
|
zero_count: !Self::ZERO_COUNT_MASK + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_zero(self) -> bool {
|
||||||
|
unsafe { self.value.is_sign_negative() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requiring `self` as a reference improves ergonomics for passing this
|
||||||
|
// method as argument to higher-order functions.
|
||||||
|
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
pub fn is_value(&self) -> bool {
|
||||||
|
!self.is_zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(self) -> f64 {
|
||||||
|
debug_assert!(self.is_value());
|
||||||
|
|
||||||
|
unsafe { self.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_value_mut(&mut self) -> &mut f64 {
|
||||||
|
debug_assert!(self.is_value());
|
||||||
|
|
||||||
|
unsafe { &mut self.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero_count(self) -> u64 {
|
||||||
|
debug_assert!(self.is_zero());
|
||||||
|
|
||||||
|
unsafe { self.zero_count & Self::ZERO_COUNT_MASK }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn incr_zero_count(&mut self) {
|
||||||
|
debug_assert!(self.is_zero());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.zero_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decr_zero_count(&mut self) {
|
||||||
|
debug_assert!(self.is_zero());
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.zero_count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
|
use crate::util::float_ext::FloatExt;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn expected(mut values in prop::collection::vec(prop::option::of(0.0..1_000.0), 0..1_000)) {
|
||||||
|
let mut vec = StrainsVec::with_capacity(values.len());
|
||||||
|
|
||||||
|
let mut additional_zeros = 0;
|
||||||
|
let mut prev_zero = false;
|
||||||
|
let mut sum = 0.0;
|
||||||
|
|
||||||
|
for opt in values.iter().copied() {
|
||||||
|
if let Some(value) = opt {
|
||||||
|
vec.push(value);
|
||||||
|
prev_zero = false;
|
||||||
|
sum += value;
|
||||||
|
} else {
|
||||||
|
vec.push(0.0);
|
||||||
|
|
||||||
|
if prev_zero {
|
||||||
|
additional_zeros += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_zero = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(vec.len(), values.len());
|
||||||
|
assert_eq!(vec.inner.len(), values.len() - additional_zeros);
|
||||||
|
assert!(vec.sum().eq(sum));
|
||||||
|
assert!(vec.iter().eq(values.iter().copied().map(|opt| opt.unwrap_or(0.0))));
|
||||||
|
|
||||||
|
values.retain(Option::is_some);
|
||||||
|
|
||||||
|
values.sort_by(|a, b| {
|
||||||
|
let (Some(a), Some(b)) = (a, b) else { unreachable!() };
|
||||||
|
|
||||||
|
b.total_cmp(a)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(vec.sorted_non_zero_iter().eq(values.into_iter().flatten()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "compact_strains"))]
|
||||||
|
mod inner {
|
||||||
|
use std::{
|
||||||
|
iter::Copied,
|
||||||
|
slice::{Iter, IterMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Plain wrapper around `Vec<f64>` because the `compact_strains` feature
|
||||||
|
/// is disabled.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StrainsVec {
|
||||||
|
inner: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrainsVec {
|
||||||
|
pub fn with_capacity(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Vec::with_capacity(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, value: f64) {
|
||||||
|
self.inner.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_desc(&mut self) {
|
||||||
|
self.inner.sort_by(|a, b| b.total_cmp(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retain_non_zero(&mut self) {
|
||||||
|
self.inner.retain(|&a| a > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retain_non_zero_and_sort(&mut self) {
|
||||||
|
self.retain_non_zero();
|
||||||
|
self.sort_desc();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn non_zero_iter(&self) -> Copied<Iter<'_, f64>> {
|
||||||
|
self.inner.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sorted_non_zero_iter(&mut self) -> Copied<Iter<'_, f64>> {
|
||||||
|
self.retain_non_zero_and_sort();
|
||||||
|
|
||||||
|
self.non_zero_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sorted_non_zero_iter_mut(&mut self) -> IterMut<'_, f64> {
|
||||||
|
self.retain_non_zero_and_sort();
|
||||||
|
|
||||||
|
self.inner.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sum(&self) -> f64 {
|
||||||
|
self.inner.iter().copied().sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> Copied<Iter<'_, f64>> {
|
||||||
|
self.inner.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_vec(self) -> Vec<f64> {
|
||||||
|
self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user