wowlab_engine_combat/state/runtime/
dense_pools.rs1use wowlab_engine_domain::pool::{SpellGate, SpellGateUpdate};
7use wowlab_types::sim::SimTime;
8
9use crate::state::LocalSpellIdx;
10
11fn local_spell_from_state_index(index: usize) -> LocalSpellIdx {
12 LocalSpellIdx::new(u8::try_from(index).expect("spell-state index fits in the local identity"))
13}
14
15#[derive(Debug, Default)]
16#[cfg(feature = "rotation-optimizations")]
17struct DirtyPoolKeys {
18 words: Box<[u64]>,
19 summary: Box<[u64]>,
20}
21
22#[cfg(feature = "rotation-optimizations")]
23impl DirtyPoolKeys {
24 fn resize(&mut self, len: usize) {
25 let word_count = len.div_ceil(u64::BITS as usize);
26
27 self.words = vec![0; word_count].into_boxed_slice();
28 self.summary = vec![0; word_count.div_ceil(u64::BITS as usize)].into_boxed_slice();
29 }
30
31 fn mark(&mut self, key: usize) {
32 let word_index = key / u64::BITS as usize;
33 let word = self
34 .words
35 .get_mut(word_index)
36 .expect("dense pool key belongs to its finalized dirty bitset");
37
38 *word |= 1_u64 << key % u64::BITS as usize;
39
40 let summary_word = self
41 .summary
42 .get_mut(word_index / u64::BITS as usize)
43 .expect("dense pool dirty word belongs to its summary bitset");
44
45 *summary_word |= 1_u64 << word_index % u64::BITS as usize;
46 }
47
48 fn pop_first(&mut self) -> Option<usize> {
49 let (summary_index, summary) = self
50 .summary
51 .iter()
52 .copied()
53 .enumerate()
54 .find(|(_, word)| *word != 0)?;
55 let word_index = summary_index * u64::BITS as usize + summary.trailing_zeros() as usize;
56 let word = self
57 .words
58 .get(word_index)
59 .copied()
60 .filter(|word| *word != 0)
61 .expect("dense pool dirty summary points to a nonempty word");
62 let key = word_index * u64::BITS as usize + word.trailing_zeros() as usize;
63 let bit = 1_u64 << key % u64::BITS as usize;
64 let word = self
65 .words
66 .get_mut(word_index)
67 .expect("dense pool dirty key belongs to its finalized bitset");
68
69 *word &= !bit;
70
71 if *word == 0 {
72 let summary_word = self
73 .summary
74 .get_mut(summary_index)
75 .expect("dense pool dirty word belongs to its summary bitset");
76
77 *summary_word &= !(1_u64 << word_index % u64::BITS as usize);
78 }
79
80 Some(key)
81 }
82}
83
84#[derive(Debug, Default)]
85pub(crate) struct DenseDamagePool {
86 values: Vec<f64>,
87 #[cfg(feature = "rotation-optimizations")]
88 dirty: DirtyPoolKeys,
89}
90
91impl DenseDamagePool {
93 pub(crate) fn resize(&mut self, len: usize) {
94 self.values.resize(len, 0.0);
95 #[cfg(feature = "rotation-optimizations")]
96 self.dirty.resize(len);
97 }
98 pub(crate) fn add(&mut self, key: usize, amount: f64) -> f64 {
99 #[cfg(feature = "rotation-optimizations")]
100 self.dirty.mark(key);
101 self.values[key] += amount;
102
103 self.values[key]
104 }
105 pub(crate) fn remaining(&self, key: usize) -> f64 {
106 self.values.get(key).copied().unwrap_or(0.0)
107 }
108 pub(crate) fn take(&mut self, key: usize) -> f64 {
109 std::mem::take(&mut self.values[key])
110 }
111 pub(crate) fn take_tick(&mut self, key: usize, ticks: f64) -> f64 {
112 let amount = self.values[key] / ticks.max(1.0);
113
114 self.values[key] = (self.values[key] - amount).max(0.0);
115
116 amount
117 }
118 pub(crate) fn remove(&mut self, key: usize) -> f64 {
119 self.take(key)
120 }
121 pub(crate) fn clear(&mut self) {
122 #[cfg(feature = "rotation-optimizations")]
123 while let Some(key) = self.dirty.pop_first() {
124 self.values[key] = 0.0;
125 }
126
127 #[cfg(not(feature = "rotation-optimizations"))]
128 self.values.fill(0.0);
129 }
130}
131
132#[derive(Debug, Default)]
133pub(crate) struct DenseRollingTickPool {
135 values: Vec<f64>,
136 #[cfg(feature = "rotation-optimizations")]
137 dirty: DirtyPoolKeys,
138}
139
140impl DenseRollingTickPool {
142 pub(crate) fn resize(&mut self, len: usize) {
143 self.values.resize(len, 1.0);
144 #[cfg(feature = "rotation-optimizations")]
145 self.dirty.resize(len);
146 }
147 pub(crate) fn mult(&self, key: usize) -> f64 {
148 self.values.get(key).copied().unwrap_or(1.0)
149 }
150 pub(crate) fn combine(&mut self, key: usize, old: f64, base: f64, new: f64, mult: f64) {
151 if new > 0.0 {
152 #[cfg(feature = "rotation-optimizations")]
153 self.dirty.mark(key);
154 self.values[key] = (old.max(0.0) * self.values[key] + base * mult) / new;
155 }
156 }
157 pub(crate) fn remove(&mut self, key: usize) {
158 self.values[key] = 1.0;
159 }
160 pub(crate) fn clear(&mut self) {
161 #[cfg(feature = "rotation-optimizations")]
162 while let Some(key) = self.dirty.pop_first() {
163 self.values[key] = 1.0;
164 }
165
166 #[cfg(not(feature = "rotation-optimizations"))]
167 self.values.fill(1.0);
168 }
169}
170
171#[derive(Debug, Default)]
173pub(crate) struct DenseExpiryPool {
174 values: Vec<Vec<SimTime>>,
175 #[cfg(feature = "rotation-optimizations")]
176 dirty: DirtyPoolKeys,
177}
178
179impl DenseExpiryPool {
181 pub(crate) fn resize(&mut self, len: usize) {
182 self.values.resize_with(len, Vec::new);
183 #[cfg(feature = "rotation-optimizations")]
184 self.dirty.resize(len);
185 }
186 pub(crate) fn push_capped(
187 &mut self,
188 key: usize,
189 expiry: SimTime,
190 capacity: usize,
191 ) -> (usize, bool) {
192 #[cfg(feature = "rotation-optimizations")]
193 self.dirty.mark(key);
194 let deadlines = &mut self.values[key];
195 let fresh = deadlines.is_empty();
196
197 if deadlines.len() >= capacity.max(1) {
198 deadlines.remove(0);
199 }
200
201 let index = deadlines.partition_point(|deadline| *deadline <= expiry);
202
203 deadlines.insert(index, expiry);
204
205 (deadlines.len(), fresh)
206 }
207 pub(crate) fn expire_due(&mut self, key: usize, now: SimTime) -> Option<(usize, usize)> {
208 let deadlines = self.values.get_mut(key)?;
209 let due = deadlines.partition_point(|deadline| *deadline <= now);
210
211 deadlines.drain(..due);
212
213 Some((due, deadlines.len()))
214 }
215 pub(crate) fn remove_oldest(&mut self, key: usize) -> bool {
216 self.values.get_mut(key).is_some_and(|v| {
217 !v.is_empty() && {
218 v.remove(0);
219
220 true
221 }
222 })
223 }
224 pub(crate) fn is_empty(&self, key: usize) -> bool {
225 self.values.get(key).is_none_or(Vec::is_empty)
226 }
227 #[cfg(test)]
228 pub(crate) fn len(&self, key: usize) -> usize {
229 self.values.get(key).map_or(0, Vec::len)
230 }
231 pub(crate) fn remove(&mut self, key: usize) {
232 self.values[key].clear();
233 }
234 pub(crate) fn clear(&mut self) {
235 #[cfg(feature = "rotation-optimizations")]
236 while let Some(key) = self.dirty.pop_first() {
237 self.values[key].clear();
238 }
239
240 #[cfg(not(feature = "rotation-optimizations"))]
241 for deadlines in &mut self.values {
242 deadlines.clear();
243 }
244 }
245}
246
247#[derive(Debug, Default)]
248pub(crate) struct DenseAuraCooldownPool {
249 values: Vec<SimTime>,
250 #[cfg(feature = "rotation-optimizations")]
251 dirty: DirtyPoolKeys,
252}
253
254impl DenseAuraCooldownPool {
256 pub(crate) fn resize(&mut self, len: usize) {
257 self.values.resize(len, SimTime::ZERO);
258 #[cfg(feature = "rotation-optimizations")]
259 self.dirty.resize(len);
260 }
261 pub(crate) fn is_ready(&self, key: usize, now: SimTime) -> bool {
262 self.values[key] <= now
263 }
264 pub(crate) fn start_for(&mut self, key: usize, now: SimTime, duration: SimTime) -> SimTime {
265 #[cfg(feature = "rotation-optimizations")]
266 self.dirty.mark(key);
267 self.values[key] = now.saturating_add(duration);
268
269 self.values[key]
270 }
271 pub(crate) fn clear(&mut self) {
272 #[cfg(feature = "rotation-optimizations")]
273 while let Some(key) = self.dirty.pop_first() {
274 self.values[key] = SimTime::ZERO;
275 }
276
277 #[cfg(not(feature = "rotation-optimizations"))]
278 self.values.fill(SimTime::ZERO);
279 }
280}
281
282#[derive(Clone, Copy, Debug, Default)]
283struct GateState {
284 base: bool,
285 blockers: u8,
286}
287
288#[derive(Debug, Default)]
289pub(crate) struct DenseSpellGatePool {
290 state: Vec<Option<GateState>>,
291 exceptions: Vec<bool>,
292}
293
294impl DenseSpellGatePool {
298 pub(crate) fn resize(&mut self, len: usize) {
299 self.state.resize(len, None);
300 self.exceptions.resize(len, false);
301 }
302 pub(crate) fn set(
303 &mut self,
304 key: LocalSpellIdx,
305 gate: SpellGate,
306 update: SpellGateUpdate,
307 ) -> bool {
308 let state = self.state[key.as_usize()].get_or_insert(GateState {
309 base: update.base_enabled,
310 blockers: 0,
311 });
312 let bit = match gate {
313 SpellGate::ReactionDelay => 1,
314 SpellGate::TargetHealth => 2,
315 SpellGate::ActiveAuraRestriction => 4,
316 _ => 8,
317 };
318
319 if update.available {
320 state.blockers &= !bit;
321 } else {
322 state.blockers |= bit;
323 }
324
325 state.base && state.blockers == 0
326 }
327 pub(crate) fn set_aura_state_exception(&mut self, key: LocalSpellIdx, active: bool) {
328 self.exceptions[key.as_usize()] = active;
329 }
330 pub(crate) fn ignores_aura_state(&self, key: LocalSpellIdx) -> bool {
331 self.exceptions[key.as_usize()]
332 }
333 pub(crate) fn drain_reset(&mut self) -> impl Iterator<Item = (LocalSpellIdx, bool)> + '_ {
334 self.exceptions.fill(false);
335
336 self.state.iter_mut().enumerate().filter_map(|(i, slot)| {
337 slot.take()
338 .map(|state| (local_spell_from_state_index(i), state.base))
339 })
340 }
341}
342
343#[cfg(all(test, feature = "rotation-optimizations"))]
344mod tests {
345 use googletest::prelude::*;
346
347 use super::*;
348
349 const FINALIZED_AURA_COUNT: usize = 65_536;
350
351 #[gtest]
352 fn sparse_aura_pool_reset_clears_only_warmed_dirty_keys_without_allocating() -> Result<()> {
353 let mut damage = DenseDamagePool::default();
354 let mut rolling = DenseRollingTickPool::default();
355 let mut expiries = DenseExpiryPool::default();
356 let mut cooldown = DenseAuraCooldownPool::default();
357
358 damage.resize(FINALIZED_AURA_COUNT);
359 rolling.resize(FINALIZED_AURA_COUNT);
360 expiries.resize(FINALIZED_AURA_COUNT);
361 cooldown.resize(FINALIZED_AURA_COUNT);
362
363 expiries.push_capped(42, SimTime::from_millis(1), 2);
364 expiries.clear();
365
366 let (_, allocations) = crate::test_alloc::allocations_during(|| {
367 damage.add(7, 12.0);
368 damage.add(42, 8.0);
369 rolling.combine(42, 1.0, 3.0, 4.0, 2.0);
370 expiries.push_capped(42, SimTime::from_millis(5), 2);
371 cooldown.start_for(42, SimTime::from_millis(10), SimTime::from_millis(20));
372
373 damage.clear();
374 rolling.clear();
375 expiries.clear();
376 cooldown.clear();
377 });
378
379 verify_that!((allocations.count, allocations.bytes), eq((0, 0)))?;
380 verify_true!(damage.dirty.summary.iter().all(|word| *word == 0))?;
381 verify_that!(damage.remaining(7), eq(0.0))?;
382 verify_that!(damage.remaining(42), eq(0.0))?;
383 verify_that!(rolling.mult(42), eq(1.0))?;
384 verify_true!(expiries.is_empty(42))?;
385
386 verify_true!(cooldown.is_ready(42, SimTime::ZERO))
387 }
388}