optimized LimitedQueue implementation

This commit is contained in:
MaxOhn
2022-07-04 17:30:27 +02:00
parent 6b410911e2
commit 1fa34258e2
8 changed files with 274 additions and 167 deletions
+2
View File
@@ -20,6 +20,8 @@
- Added a new field `kiai: bool` to both `TimingPoint` and `DifficultyPoint` to denote whether the current timing section is in kiai mode - Added a new field `kiai: bool` to both `TimingPoint` and `DifficultyPoint` to denote whether the current timing section is in kiai mode
- Added a new field `breaks: Vec<Break>` to `Beatmap` that contains all breaks throughout the map - Added a new field `breaks: Vec<Break>` to `Beatmap` that contains all breaks throughout the map
- Added a new field `edge_sounds: Vec<u8>` to the `Slider` variant of `HitObjectKind` to denote the sample played on slider heads, ends, and repeats - Added a new field `edge_sounds: Vec<u8>` to the `Slider` variant of `HitObjectKind` to denote the sample played on slider heads, ends, and repeats
- __Other:__
- Small performance improvements for osu!taiko calculations
# v0.5.2 (2022-06-14) # v0.5.2 (2022-06-14)
+1 -1
View File
@@ -65,7 +65,7 @@ impl Beatmap {
map.cs = target_columns; map.cs = target_columns;
let mut prev_note_times = LimitedQueue::new(MAX_NOTES_FOR_DENSITY); let mut prev_note_times: LimitedQueue<f64, MAX_NOTES_FOR_DENSITY> = LimitedQueue::new();
let mut density = i32::MAX as f64; let mut density = i32::MAX as f64;
let mut compute_density = |new_note_time: f64, d: &mut f64| { let mut compute_density = |new_note_time: f64, d: &mut f64| {
@@ -113,7 +113,7 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
let conversion_diff = self.conversion_difficulty(); let conversion_diff = self.conversion_difficulty();
if self.total_columns == 1 { if self.total_columns == 1 {
return Pattern::new_slider_note(self, 0, self.start_time, self.end_time); Pattern::new_slider_note(self, 0, self.start_time, self.end_time)
} else if self.span_count > 1 { } else if self.span_count > 1 {
if self.segment_duration <= 90 { if self.segment_duration <= 90 {
self.generate_random_hold_notes(self.start_time, 1) self.generate_random_hold_notes(self.start_time, 1)
+1
View File
@@ -37,6 +37,7 @@ use crate::{
/// } /// }
/// ``` /// ```
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum GradualDifficultyAttributes<'map> { pub enum GradualDifficultyAttributes<'map> {
/// Gradual osu!catch difficulty attributes. /// Gradual osu!catch difficulty attributes.
Catch(CatchGradualDifficultyAttributes<'map>), Catch(CatchGradualDifficultyAttributes<'map>),
+115 -43
View File
@@ -1,75 +1,101 @@
use std::cmp::Ordering; use std::{
use std::iter::{Cycle, Skip, Take}; cmp::Ordering,
use std::ops::Index; iter::{Cycle, Skip, Take},
use std::slice::Iter; ops::Index,
slice::Iter,
};
// TODO: make generic over const size /// Efficient counterpart to osu!'s [`LimitedCapacityQueue`]
/// i.e. an indexed queue with limited capacity.
///
/// [`LimitedQueue`] will use an internal array as queue which
/// is stored on the stack. Hence, if the size is very large,
/// e.g. `size_of<T>() * N`, consider using a different type
/// since heap allocation might be favorable.
///
/// [`LimitedCapacityQueue`]: https://github.com/ppy/osu/blob/b49a1aab8ac6e16e48dffd03f55635cdc1771adf/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct LimitedQueue<T> { pub(crate) struct LimitedQueue<T, const N: usize> {
queue: Vec<T>, queue: [T; N],
start: usize, /// If the queue is not empty, `end` is the index of the last element.
/// Otherwise, it has no meaning.
end: usize, end: usize,
/// Amount of elements in the queue. This is equal to `end + 1`
/// if the queue is not full, or `N` otherwise.
len: usize,
} }
impl<T> LimitedQueue<T> { impl<T, const N: usize> Default for LimitedQueue<T, N>
where
T: Copy + Clone + Default,
{
#[inline] #[inline]
pub(crate) fn new(capacity: usize) -> Self { fn default() -> Self {
Self { Self {
end: capacity - 1, end: N - 1,
start: 0, queue: [T::default(); N],
queue: Vec::with_capacity(capacity), len: 0,
} }
} }
}
#[inline] impl<T, const N: usize> LimitedQueue<T, N>
where
T: Copy + Clone + Default,
{
pub(crate) fn new() -> Self {
Self::default()
}
}
impl<T, const N: usize> LimitedQueue<T, N> {
pub(crate) fn push(&mut self, elem: T) { pub(crate) fn push(&mut self, elem: T) {
let capacity = self.queue.capacity(); self.end = (self.end + 1) % N;
self.end = (self.end + 1) % capacity; self.queue[self.end] = elem;
self.len += (self.len < N) as usize;
}
if self.queue.len() == capacity { pub(crate) fn is_empty(&self) -> bool {
self.start = (self.start + 1) % capacity; self.len == 0
self.queue[self.end] = elem; }
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn last(&self) -> Option<&T> {
if self.is_empty() {
None
} else { } else {
self.queue.push(elem); Some(&self.queue[self.end])
} }
} }
#[inline]
pub(crate) fn len(&self) -> usize {
self.queue.len()
}
#[inline]
pub(crate) fn last(&self) -> Option<&T> {
self.queue.get(self.end)
}
#[inline]
pub(crate) fn clear(&mut self) { pub(crate) fn clear(&mut self) {
self.start = 0; self.end = N - 1;
self.end = self.queue.capacity() - 1; self.len = 0;
self.queue.clear();
} }
#[inline]
pub(crate) fn full(&self) -> bool { pub(crate) fn full(&self) -> bool {
self.queue.len() == self.queue.capacity() self.len == N
} }
#[inline]
pub(crate) fn iter(&self) -> LimitedQueueIter<'_, T> { pub(crate) fn iter(&self) -> LimitedQueueIter<'_, T> {
self.queue self.queue
.iter() .iter()
.cycle() .cycle()
.skip(self.start) .skip((self.len == N) as usize * (self.end + 1))
.take(self.queue.len()) .take(self.len)
} }
} }
impl<T: PartialOrd> LimitedQueue<T> { pub(crate) type LimitedQueueIter<'a, T> = Take<Skip<Cycle<Iter<'a, T>>>>;
impl<T: PartialOrd, const N: usize> LimitedQueue<T, N> {
pub(crate) fn min(&self) -> Option<&T> { pub(crate) fn min(&self) -> Option<&T> {
self.queue self.queue
.iter() .iter()
.take(self.len)
.reduce(|min, next| match min.partial_cmp(next) { .reduce(|min, next| match min.partial_cmp(next) {
Some(Ordering::Less) => min, Some(Ordering::Less) => min,
Some(Ordering::Equal) => min, Some(Ordering::Equal) => min,
@@ -79,13 +105,59 @@ impl<T: PartialOrd> LimitedQueue<T> {
} }
} }
impl<T> Index<usize> for LimitedQueue<T> { impl<T, const N: usize> Index<usize> for LimitedQueue<T, N> {
type Output = T; type Output = T;
#[inline] #[inline]
fn index(&self, idx: usize) -> &Self::Output { fn index(&self, idx: usize) -> &Self::Output {
&self.queue[(self.start + idx) % self.queue.capacity()] assert!(
idx < self.len,
"index out of bounds: the len is {} but the index is {idx}",
self.len
);
let idx = (idx + (self.len == N) as usize * (self.end + 1)) % N;
&self.queue[idx]
} }
} }
pub(crate) type LimitedQueueIter<'a, T> = Take<Skip<Cycle<Iter<'a, T>>>>; #[cfg(test)]
mod test {
use super::LimitedQueue;
#[test]
fn empty() {
let queue = LimitedQueue::<u8, 4>::default();
assert!(queue.is_empty());
assert_eq!(queue.last(), None);
assert_eq!(queue.iter().count(), 0);
}
#[test]
fn single_push() {
let mut queue = LimitedQueue::<u8, 4>::default();
let elem = 42;
queue.push(elem);
assert!(!queue.is_empty());
assert_eq!(queue.len(), 1);
assert_eq!(queue.last(), Some(&elem));
assert!(queue.iter().eq(vec![elem].iter()));
assert_eq!(queue[0], elem);
}
#[test]
fn overfull() {
let mut queue = LimitedQueue::<u8, 4>::default();
for i in 1..=5 {
queue.push(i as u8);
assert_eq!(i.min(4), queue.len());
}
assert_eq!(queue.last(), Some(&5));
assert!(queue.iter().eq(vec![2, 3, 4, 5].iter()));
assert_eq!(queue[0], 2);
assert_eq!(queue[3], 5);
}
}
+94 -88
View File
@@ -1,88 +1,94 @@
use crate::parse::HitObject; use crate::parse::HitObject;
use std::cmp::Ordering; use std::cmp::Ordering;
static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [ static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
HitObjectRhythm { HitObjectRhythm {
id: 0, id: 0,
ratio: 1.0, ratio: 1.0,
difficulty: 0.0, difficulty: 0.0,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 1, id: 1,
ratio: 2.0 / 1.0, ratio: 2.0 / 1.0,
difficulty: 0.3, difficulty: 0.3,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 2, id: 2,
ratio: 1.0 / 2.0, ratio: 1.0 / 2.0,
difficulty: 0.5, difficulty: 0.5,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 3, id: 3,
ratio: 3.0 / 1.0, ratio: 3.0 / 1.0,
difficulty: 0.3, difficulty: 0.3,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 4, id: 4,
ratio: 1.0 / 3.0, ratio: 1.0 / 3.0,
difficulty: 0.35, difficulty: 0.35,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 5, id: 5,
ratio: 3.0 / 2.0, ratio: 3.0 / 2.0,
difficulty: 0.6, difficulty: 0.6,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 6, id: 6,
ratio: 2.0 / 3.0, ratio: 2.0 / 3.0,
difficulty: 0.4, difficulty: 0.4,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 7, id: 7,
ratio: 5.0 / 4.0, ratio: 5.0 / 4.0,
difficulty: 0.5, difficulty: 0.5,
}, },
HitObjectRhythm { HitObjectRhythm {
id: 8, id: 8,
ratio: 4.0 / 5.0, ratio: 4.0 / 5.0,
difficulty: 0.7, difficulty: 0.7,
}, },
]; ];
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub(crate) struct HitObjectRhythm { pub(crate) struct HitObjectRhythm {
id: u8, id: u8,
ratio: f64, ratio: f64,
pub(crate) difficulty: f64, pub(crate) difficulty: f64,
} }
impl PartialEq for HitObjectRhythm { impl HitObjectRhythm {
#[inline] pub(crate) fn static_ref() -> &'static Self {
fn eq(&self, other: &Self) -> bool { &COMMON_RHYTHMS[0]
self.id == other.id }
} }
}
impl PartialEq for HitObjectRhythm {
impl Eq for HitObjectRhythm {} #[inline]
fn eq(&self, other: &Self) -> bool {
#[inline] self.id == other.id
pub(crate) fn closest_rhythm( }
delta_time: f64, }
last: &HitObject,
last_last: &HitObject, impl Eq for HitObjectRhythm {}
clock_rate: f64,
) -> &'static HitObjectRhythm { #[inline]
let prev_len = (last.start_time - last_last.start_time) / clock_rate; pub(crate) fn closest_rhythm(
let ratio = delta_time / prev_len; delta_time: f64,
last: &HitObject,
COMMON_RHYTHMS last_last: &HitObject,
.iter() clock_rate: f64,
.min_by(|r1, r2| { ) -> &'static HitObjectRhythm {
(r1.ratio - ratio) let prev_len = (last.start_time - last_last.start_time) / clock_rate;
.abs() let ratio = delta_time / prev_len;
.partial_cmp(&(r2.ratio - ratio).abs())
.unwrap_or(Ordering::Equal) COMMON_RHYTHMS
}) .iter()
.unwrap() .min_by(|r1, r2| {
} (r1.ratio - ratio)
.abs()
.partial_cmp(&(r2.ratio - ratio).abs())
.unwrap_or(Ordering::Equal)
})
.unwrap()
}
+34 -13
View File
@@ -2,8 +2,6 @@ use crate::limited_queue::LimitedQueue;
use super::{DifficultyObject, HitObjectRhythm, Rim}; use super::{DifficultyObject, HitObjectRhythm, Rim};
use std::ops::Index;
const RHYTHM_STRAIN_DECAY: f64 = 0.96; const RHYTHM_STRAIN_DECAY: f64 = 0.96;
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2; const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
@@ -11,20 +9,44 @@ const MONO_HISTORY_MAX_LEN: usize = 5;
const RHYTHM_HISTORY_MAX_LEN: usize = 8; const RHYTHM_HISTORY_MAX_LEN: usize = 8;
const STAMINA_HISTORY_MAX_LEN: usize = 2; const STAMINA_HISTORY_MAX_LEN: usize = 2;
#[derive(Copy, Clone, Debug)]
pub(crate) struct RhythmHistoryElement {
idx: usize,
rhythm: &'static HitObjectRhythm,
}
impl RhythmHistoryElement {
fn new(difficulty_object: &DifficultyObject<'_>) -> Self {
Self {
idx: difficulty_object.idx,
rhythm: difficulty_object.rhythm,
}
}
}
impl Default for RhythmHistoryElement {
fn default() -> Self {
Self {
idx: 0,
rhythm: HitObjectRhythm::static_ref(),
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum SkillKind { pub(crate) enum SkillKind {
Color { Color {
mono_history: LimitedQueue<usize>, mono_history: LimitedQueue<usize, MONO_HISTORY_MAX_LEN>,
prev_is_rim: Option<bool>, prev_is_rim: Option<bool>,
current_mono_len: usize, current_mono_len: usize,
}, },
Rhythm { Rhythm {
rhythm_history: LimitedQueue<(usize, &'static HitObjectRhythm)>, // (idx, rhythm) rhythm_history: LimitedQueue<RhythmHistoryElement, RHYTHM_HISTORY_MAX_LEN>,
notes_since_rhythm_change: usize, notes_since_rhythm_change: usize,
current_strain: f64, current_strain: f64,
}, },
Stamina { Stamina {
note_pair_duration_history: LimitedQueue<f64>, note_pair_duration_history: LimitedQueue<f64, STAMINA_HISTORY_MAX_LEN>,
hand: u8, hand: u8,
off_hand_object_duration: f64, off_hand_object_duration: f64,
}, },
@@ -34,7 +56,7 @@ impl SkillKind {
#[inline] #[inline]
pub(crate) fn color() -> Self { pub(crate) fn color() -> Self {
Self::Color { Self::Color {
mono_history: LimitedQueue::new(MONO_HISTORY_MAX_LEN), mono_history: LimitedQueue::new(),
prev_is_rim: None, prev_is_rim: None,
current_mono_len: 0, current_mono_len: 0,
} }
@@ -43,7 +65,7 @@ impl SkillKind {
#[inline] #[inline]
pub(crate) fn rhythm() -> Self { pub(crate) fn rhythm() -> Self {
Self::Rhythm { Self::Rhythm {
rhythm_history: LimitedQueue::new(RHYTHM_HISTORY_MAX_LEN), rhythm_history: LimitedQueue::new(),
notes_since_rhythm_change: 0, notes_since_rhythm_change: 0,
current_strain: 0.0, current_strain: 0.0,
} }
@@ -52,7 +74,7 @@ impl SkillKind {
#[inline] #[inline]
pub(crate) fn stamina(right_hand: bool) -> Self { pub(crate) fn stamina(right_hand: bool) -> Self {
Self::Stamina { Self::Stamina {
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN), note_pair_duration_history: LimitedQueue::new(),
hand: right_hand as u8, hand: right_hand as u8,
off_hand_object_duration: f64::MAX, off_hand_object_duration: f64::MAX,
} }
@@ -114,7 +136,7 @@ impl SkillKind {
let to_compare = let to_compare =
mono_history.len() + i - MOST_RECENT_PATTERNS_TO_COMPARE; mono_history.len() + i - MOST_RECENT_PATTERNS_TO_COMPARE;
mono_history.index(start + i) != mono_history.index(to_compare) mono_history[start + i] != mono_history[to_compare]
}); });
if different_pattern { if different_pattern {
@@ -166,7 +188,7 @@ impl SkillKind {
let mut strain = current.rhythm.difficulty; let mut strain = current.rhythm.difficulty;
rhythm_history.push((current.idx, current.rhythm)); rhythm_history.push(RhythmHistoryElement::new(current));
let mut reps_penalty = 1.0; let mut reps_penalty = 1.0;
@@ -181,15 +203,14 @@ impl SkillKind {
let to_compare = let to_compare =
rhythm_history.len() + i - most_recent_patterns_to_compare; rhythm_history.len() + i - most_recent_patterns_to_compare;
rhythm_history.index(start + i).1 != rhythm_history.index(to_compare).1 rhythm_history[start + i].rhythm != rhythm_history[to_compare].rhythm
}); });
if different_pattern { if different_pattern {
continue; continue;
} }
reps_penalty *= reps_penalty *= repetition_penalty(current.idx - rhythm_history[start].idx);
repetition_penalty(current.idx - rhythm_history.index(start).0);
break; break;
} }
+26 -21
View File
@@ -6,27 +6,33 @@ const TL_MIN_REPETITIONS: isize = 16;
pub(crate) trait StaminaCheeseDetector { pub(crate) trait StaminaCheeseDetector {
fn find_cheese(&self) -> Vec<bool>; fn find_cheese(&self) -> Vec<bool>;
fn find_rolls(&self, pattern_len: usize, cheese: &mut [bool]); fn find_rolls<const PATTERN_LEN: usize, const DOUBLE_PATTERN_LEN: usize>(
fn find_tl_tap(&self, parity: usize, is_rin: bool, cheese: &mut [bool]); &self,
cheese: &mut [bool],
);
fn find_tl_tap<const PARITY: usize, const IS_RIN: bool>(&self, cheese: &mut [bool]);
} }
impl StaminaCheeseDetector for Beatmap { impl StaminaCheeseDetector for Beatmap {
fn find_cheese(&self) -> Vec<bool> { fn find_cheese(&self) -> Vec<bool> {
let mut cheese = vec![false; self.hit_objects.len()]; let mut cheese = vec![false; self.hit_objects.len()];
self.find_rolls(3, &mut cheese); self.find_rolls::<3, 6>(&mut cheese);
self.find_rolls(4, &mut cheese); self.find_rolls::<4, 8>(&mut cheese);
self.find_tl_tap(0, true, &mut cheese); self.find_tl_tap::<0, true>(&mut cheese);
self.find_tl_tap(1, true, &mut cheese); self.find_tl_tap::<1, true>(&mut cheese);
self.find_tl_tap(0, false, &mut cheese); self.find_tl_tap::<0, false>(&mut cheese);
self.find_tl_tap(1, false, &mut cheese); self.find_tl_tap::<1, false>(&mut cheese);
cheese cheese
} }
fn find_rolls(&self, pattern_len: usize, cheese: &mut [bool]) { fn find_rolls<const PATTERN_LEN: usize, const DOUBLE_PATTERN_LEN: usize>(
let mut history = LimitedQueue::new(2 * pattern_len); &self,
cheese: &mut [bool],
) {
let mut history: LimitedQueue<u8, DOUBLE_PATTERN_LEN> = LimitedQueue::new();
let mut index_before_last_repeat = -1; let mut index_before_last_repeat = -1;
let mut last_mark_end = 0; let mut last_mark_end = 0;
@@ -38,7 +44,7 @@ impl StaminaCheeseDetector for Beatmap {
continue; continue;
} }
let contains = contains_pattern_repeat(&history, pattern_len); let contains = contains_pattern_repeat::<PATTERN_LEN, DOUBLE_PATTERN_LEN>(&history);
if !contains { if !contains {
index_before_last_repeat = (i + 1 - history.len()) as isize; index_before_last_repeat = (i + 1 - history.len()) as isize;
@@ -58,12 +64,12 @@ impl StaminaCheeseDetector for Beatmap {
} }
} }
fn find_tl_tap(&self, parity: usize, is_rin: bool, cheese: &mut [bool]) { fn find_tl_tap<const PARITY: usize, const IS_RIN: bool>(&self, cheese: &mut [bool]) {
let mut tl_len = -2; let mut tl_len = -2;
let mut last_mark_end = 0; let mut last_mark_end = 0;
for (i, &sound) in self.sounds.iter().enumerate().skip(parity).step_by(2) { for (i, &sound) in self.sounds.iter().enumerate().skip(PARITY).step_by(2) {
if sound.is_rim() == is_rin { if sound.is_rim() == IS_RIN {
tl_len += 2; tl_len += 2;
} else { } else {
tl_len = -2; tl_len = -2;
@@ -73,11 +79,8 @@ impl StaminaCheeseDetector for Beatmap {
continue; continue;
} }
mark_as_cheese( let start = (i as isize + 1 - tl_len).max(last_mark_end as isize);
(i as isize + 1 - tl_len).max(last_mark_end as isize) as usize, mark_as_cheese(start as usize, i, cheese);
i,
cheese,
);
last_mark_end = i; last_mark_end = i;
} }
@@ -94,8 +97,10 @@ fn mark_as_cheese(start: usize, end: usize, cheese: &mut [bool]) {
} }
#[inline] #[inline]
fn contains_pattern_repeat(history: &LimitedQueue<u8>, pattern_len: usize) -> bool { fn contains_pattern_repeat<const PATTERN_LEN: usize, const DOUBLE_PATTERN_LEN: usize>(
for (&curr, &to_compare) in history.iter().zip(history.iter().skip(pattern_len)) { history: &LimitedQueue<u8, DOUBLE_PATTERN_LEN>,
) -> bool {
for (&curr, &to_compare) in history.iter().zip(history.iter().skip(PATTERN_LEN)) {
if curr.is_rim() != to_compare.is_rim() { if curr.is_rim() != to_compare.is_rim() {
return false; return false;
} }