Skip to main content

wowlab_engine_domain/rotation/buffer/dense/
accessors.rs

1use bytemuck::{Pod, Zeroable};
2use wowlab_buffer_contract::SLOT_ALIGNMENT;
3use wowlab_types::{
4    combat::ResourceType,
5    sim::{AuraKey, AuraProjectionKey, SpellIdx},
6};
7
8use super::{BufferOffsets, HistoryIterMut};
9use crate::rotation::buffer::{
10    AuraSlot, ByteOffset, CombatSlot, CooldownSlot, DefaultValue, EquipmentSlot, HeroTreeSlot,
11    HistorySlot, ItemSlot, PetSlot, PlayerSlot, ResourceSlot, SetBonusSlot, SpellSlot,
12    StandaloneDefault, SwingSlot, TalentSlot, UnitSlot, WeaponImbueSlot,
13};
14
15/// `repr(align(8))` backing chunk so the `Vec<SlotChunk>` allocation is `SLOT_ALIGNMENT`-aligned.
16#[derive(Clone, Copy, Debug)]
17#[repr(C, align(8))]
18struct SlotChunk([u8; SLOT_ALIGNMENT]);
19
20// SAFETY: plain `[u8; N]` with no padding is transparently zeroable.
21unsafe impl Zeroable for SlotChunk {}
22// SAFETY: every bit pattern of `[u8; N]` is a valid value.
23unsafe impl Pod for SlotChunk {}
24
25const _: () = assert!(
26    size_of::<SlotChunk>() == SLOT_ALIGNMENT,
27    "SlotChunk must be exactly SLOT_ALIGNMENT bytes wide"
28);
29
30const _: () = assert!(
31    align_of::<SlotChunk>() == SLOT_ALIGNMENT,
32    "SlotChunk must be SLOT_ALIGNMENT-aligned"
33);
34
35#[inline]
36fn chunks_for_bytes(byte_len: usize) -> usize {
37    byte_len.div_ceil(SLOT_ALIGNMENT)
38}
39
40macro_rules! singleton_slot {
41    ($read:ident, $write:ident, $Slot:ty, $field:ident) => {
42        #[inline(always)]
43        pub fn $read(&self) -> &$Slot {
44            self.slot_ref::<$Slot>(self.offsets.$field)
45        }
46        #[inline(always)]
47        pub fn $write(&mut self) -> &mut $Slot {
48            self.slot_mut::<$Slot>(self.offsets.$field)
49        }
50    };
51}
52
53macro_rules! keyed_slot {
54    ($read:ident, $write:ident, $Slot:ty, $Key:ty, $map:ident) => {
55        #[inline]
56        pub fn $read(&self, key: $Key) -> Option<&$Slot> {
57            let &base = self.offsets.slots.$map.get(&key)?;
58            Some(self.slot_ref::<$Slot>(base))
59        }
60        #[inline]
61        pub fn $write(&mut self, key: $Key) -> Option<&mut $Slot> {
62            let &base = self.offsets.slots.$map.get(&key)?;
63            Some(self.slot_mut::<$Slot>(base))
64        }
65    };
66}
67
68macro_rules! keyed_slot_str {
69    ($read:ident, $write:ident, $Slot:ty, $map:ident) => {
70        #[inline]
71        pub fn $read(&self, key: &str) -> Option<&$Slot> {
72            let &base = self.offsets.slots.$map.get(key)?;
73            Some(self.slot_ref::<$Slot>(base))
74        }
75        #[inline]
76        pub fn $write(&mut self, key: &str) -> Option<&mut $Slot> {
77            let &base = self.offsets.slots.$map.get(key)?;
78            Some(self.slot_mut::<$Slot>(base))
79        }
80    };
81}
82
83macro_rules! ensure_slot {
84    ($fn:ident, $Slot:ty, $map:ident, $key:ident : $Key:ty, $insert:expr, $lookup:expr) => {
85        #[doc = concat!(
86                            "Ensure a `", stringify!($Slot), "` exists for `", stringify!($key),
87                            "`, returning `true` if it already existed."
88                        )]
89        pub fn $fn(&mut self, $key: $Key) -> bool {
90            if self.offsets.slots.$map.contains_key($lookup) {
91                return true;
92            }
93            let base = self.align_to_slot();
94            self.resize_bytes(base + <$Slot>::SIZE);
95            self.offsets.slots.$map.insert($insert, ByteOffset::new(base));
96            false
97        }
98    };
99}
100
101/// Flat byte buffer holding all runtime rotation state for zero-allocation evaluation.
102#[derive(Debug)]
103pub struct DenseBuffer {
104    chunks: Vec<SlotChunk>,
105    byte_len: usize,
106    pub(super) offsets: BufferOffsets,
107    standalone_defaults: Vec<(ByteOffset, &'static DefaultValue)>,
108}
109
110impl DenseBuffer {
111    #[must_use]
112    pub fn new(offsets: BufferOffsets, size: usize) -> Self {
113        let mut standalone_defaults = Vec::with_capacity(offsets.standalone.len());
114
115        for (&descriptor_id, &offset) in &offsets.standalone {
116            if let Some(default) = inventory::iter::<StandaloneDefault>
117                .into_iter()
118                .find(|d| (d.matches)(descriptor_id))
119            {
120                standalone_defaults.push((offset, &default.value));
121            }
122        }
123
124        Self {
125            chunks: vec![SlotChunk([0u8; SLOT_ALIGNMENT]); chunks_for_bytes(size)],
126            byte_len: size,
127            offsets,
128            standalone_defaults,
129        }
130    }
131
132    /// Snapshot the concrete aura identities currently allocated in this buffer.
133    pub fn aura_keys(&self) -> impl Iterator<Item = AuraKey> + '_ {
134        self.offsets.slots.auras.keys().copied()
135    }
136    /// Snapshot the APL aura category projections allocated by the rotation schema.
137    pub fn aura_projection_keys(&self) -> impl Iterator<Item = AuraProjectionKey> + '_ {
138        self.offsets.slots.aura_projections.keys().copied()
139    }
140
141    #[inline]
142    #[must_use]
143    pub fn as_ptr(&self) -> *const u8 {
144        self.chunks.as_ptr().cast::<u8>()
145    }
146
147    #[inline]
148    pub fn as_mut_ptr(&mut self) -> *mut u8 {
149        self.chunks.as_mut_ptr().cast::<u8>()
150    }
151
152    /// Active byte view truncated to the schema-declared `byte_len`.
153    #[inline]
154    #[must_use]
155    pub fn bytes(&self) -> &[u8] {
156        // #t(rust_unchecked_indexing) byte_len is maintained <= chunks.len() * SLOT_ALIGNMENT by resize_bytes
157        &bytemuck::cast_slice::<SlotChunk, u8>(&self.chunks)[..self.byte_len]
158    }
159
160    /// Active mutable byte view. See [`Self::bytes`].
161    #[inline]
162    pub fn bytes_mut(&mut self) -> &mut [u8] {
163        let len = self.byte_len;
164
165        // #t(rust_unchecked_indexing) byte_len is maintained <= chunks.len() * SLOT_ALIGNMENT by resize_bytes
166        &mut bytemuck::cast_slice_mut::<SlotChunk, u8>(&mut self.chunks)[..len]
167    }
168
169    /// Mutable in-place iterator over every [`HistorySlot`] without allocating a key list.
170    pub fn history_iter_mut(&mut self) -> HistoryIterMut<'_> {
171        let mut offsets: Vec<usize> = self
172            .offsets
173            .slots
174            .history
175            .values()
176            .map(ByteOffset::as_usize)
177            .collect();
178
179        offsets.sort_unstable();
180
181        HistoryIterMut {
182            rest: self.bytes_mut(),
183            offsets: offsets.into_iter(),
184            consumed: 0,
185        }
186    }
187    /// Clear every history slot's previous-cast flags (`prev_gcd` / `prev_off_gcd`).
188    pub fn clear_history(&mut self) {
189        for h in self.history_iter_mut() {
190            h.prev_gcd = 0;
191            h.prev_off_gcd = 0;
192        }
193    }
194    /// Zero the buffer and write standalone field defaults.
195    pub fn reset_and_defaults(&mut self) {
196        self.bytes_mut().fill(0);
197        self.populate_standalone_defaults();
198    }
199    #[inline]
200    fn slot_ref<T>(&self, base: ByteOffset) -> &T
201    where
202        T: Pod,
203    {
204        let base = base.as_usize();
205
206        // #t(rust_unchecked_indexing) base + slot size is schema-validated to lie within byte_len
207        bytemuck::from_bytes(&self.bytes()[base..base + size_of::<T>()])
208    }
209    #[inline]
210    fn slot_mut<T>(&mut self, base: ByteOffset) -> &mut T
211    where
212        T: Pod,
213    {
214        let base = base.as_usize();
215
216        // #t(rust_unchecked_indexing) base + slot size is schema-validated to lie within byte_len
217        bytemuck::from_bytes_mut(&mut self.bytes_mut()[base..base + size_of::<T>()])
218    }
219    fn resize_bytes(&mut self, new_byte_len: usize) {
220        let needed_chunks = chunks_for_bytes(new_byte_len);
221
222        if needed_chunks > self.chunks.len() {
223            self.chunks
224                .resize(needed_chunks, SlotChunk([0u8; SLOT_ALIGNMENT]));
225        }
226
227        let prev_len = self.byte_len;
228
229        if new_byte_len > prev_len {
230            let bytes = bytemuck::cast_slice_mut::<SlotChunk, u8>(&mut self.chunks);
231
232            // #t(rust_unchecked_indexing) needed_chunks above guarantees bytes.len() >= new_byte_len
233            for byte in &mut bytes[prev_len..new_byte_len] {
234                *byte = 0;
235            }
236        }
237
238        self.byte_len = new_byte_len;
239    }
240    fn align_to_slot(&mut self) -> usize {
241        let cur = self.byte_len;
242
243        if cur % SLOT_ALIGNMENT != 0 {
244            self.resize_bytes(cur + (SLOT_ALIGNMENT - (cur % SLOT_ALIGNMENT)));
245        }
246
247        self.byte_len
248    }
249    fn populate_standalone_defaults(&mut self) {
250        let byte_len = self.byte_len;
251        // #t(rust_unchecked_indexing) byte_len is maintained <= chunks.len() * SLOT_ALIGNMENT by resize_bytes
252        let bytes = &mut bytemuck::cast_slice_mut::<SlotChunk, u8>(&mut self.chunks)[..byte_len];
253
254        for &(offset, value) in &self.standalone_defaults {
255            value.write_to(bytes, offset);
256        }
257    }
258    singleton_slot!(player, player_mut, PlayerSlot, player);
259    singleton_slot!(combat, combat_mut, CombatSlot, combat);
260    singleton_slot!(pet, pet_mut, PetSlot, pet);
261    keyed_slot!(cooldown, cooldown_mut, CooldownSlot, SpellIdx, cooldowns);
262    keyed_slot!(aura, aura_mut, AuraSlot, AuraKey, auras);
263    keyed_slot!(
264        aura_projection,
265        aura_projection_mut,
266        AuraSlot,
267        AuraProjectionKey,
268        aura_projections
269    );
270    keyed_slot!(
271        resource,
272        resource_mut,
273        ResourceSlot,
274        ResourceType,
275        resources
276    );
277
278    keyed_slot!(spell, spell_mut, SpellSlot, SpellIdx, spells);
279
280    keyed_slot!(history, history_mut, HistorySlot, SpellIdx, history);
281
282    keyed_slot_str!(talent, talent_mut, TalentSlot, talents);
283    keyed_slot_str!(hero_tree, hero_tree_mut, HeroTreeSlot, hero_trees);
284    keyed_slot_str!(item, item_mut, ItemSlot, items);
285    keyed_slot_str!(swing, swing_mut, SwingSlot, swings);
286    keyed_slot_str!(equipment, equipment_mut, EquipmentSlot, equipment);
287    keyed_slot_str!(set_bonus, set_bonus_mut, SetBonusSlot, set_bonuses);
288    keyed_slot_str!(unit, unit_mut, UnitSlot, units);
289    keyed_slot_str!(
290        weapon_imbue,
291        weapon_imbue_mut,
292        WeaponImbueSlot,
293        weapon_imbues
294    );
295
296    ensure_slot!(ensure_swing_slot, SwingSlot, swings, key: &str, key.to_string(), key);
297
298    ensure_slot!(ensure_unit_slot, UnitSlot, units, key: &str, key.to_string(), key);
299
300    ensure_slot!(
301        ensure_weapon_imbue_slot,
302        WeaponImbueSlot,
303        weapon_imbues,
304        key: &str,
305        key.to_string(),
306        key
307    );
308
309    ensure_slot!(ensure_aura_slot, AuraSlot, auras, key: AuraKey, key, &key);
310
311    ensure_slot!(
312        ensure_aura_projection_slot,
313        AuraSlot,
314        aura_projections,
315        key: AuraProjectionKey,
316        key,
317        &key
318    );
319
320    ensure_slot!(
321        ensure_spell_slot,
322        SpellSlot,
323        spells,
324        spell: SpellIdx,
325        spell,
326        &spell
327    );
328
329    ensure_slot!(
330        ensure_cooldown_slot,
331        CooldownSlot,
332        cooldowns,
333        spell: SpellIdx,
334        spell,
335        &spell
336    );
337
338    ensure_slot!(
339        ensure_resource_slot,
340        ResourceSlot,
341        resources,
342        resource: ResourceType,
343        resource,
344        &resource
345    );
346}