1use celox_design::{
7 AbsoluteAddrBase, RegionedAbsoluteAddrBase, RuntimeEventSite, SPARSE_WORKING_REGION,
8 STABLE_REGION,
9};
10use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
11use serde::{Deserialize, Serialize};
12use std::hash::Hash;
13
14mod trace;
15pub use trace::{TRACE_GROUP_BYTES, TraceLayout};
16
17pub const RUNTIME_EVENT_CAPACITY: usize = 1024;
18pub const RUNTIME_EVENT_WRITING: u64 = u64::MAX;
19pub const STATE_HEADER_SIZE: usize = 32;
20pub const STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET: usize = 0;
21#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
23pub const STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET: usize = 8;
24#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
25pub const STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET: usize = 24;
26#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
27pub const STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET: usize = 16;
28pub const RUNTIME_EVENT_HEADER_SIZE: usize = 8;
30pub const RUNTIME_EVENT_SLOT_SEQ_OFFSET: usize = 0;
31pub const RUNTIME_EVENT_SLOT_SITE_OFFSET: usize = 8;
32pub const RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET: usize = 16;
33pub const RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET: usize = 24;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RuntimeEventArgLayout {
37 pub value_word_offset: usize,
38 pub mask_word_offset: usize,
39 pub word_count: usize,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RuntimeEventSiteLayout {
44 pub args: Vec<RuntimeEventArgLayout>,
45 pub payload_words: usize,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SparseWorkingLayout {
50 pub active_index: usize,
51 pub chunk_count: usize,
52 pub dirty_words_offset: usize,
53 pub dirty_word_count: usize,
54 pub summary_words_offset: usize,
55 pub summary_word_count: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum MemoryLayoutMode {
60 Packed,
61 ElementStrided,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub struct UnpackedArrayLayout {
66 pub element_width: usize,
67 pub element_count: usize,
68 pub element_stride: usize,
69 pub plane_size: usize,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct StateObjectLayout<A> {
75 pub address: A,
76 pub width: usize,
77 pub is_4state: bool,
78}
79
80#[derive(Debug, Clone)]
87pub struct LayoutRequirements<A> {
88 state_aliases: HashMap<A, A>,
89}
90
91impl<A> Default for LayoutRequirements<A> {
92 fn default() -> Self {
93 Self {
94 state_aliases: HashMap::default(),
95 }
96 }
97}
98
99impl<A> LayoutRequirements<A> {
100 pub fn state_aliases(&self) -> &HashMap<A, A> {
101 &self.state_aliases
102 }
103
104 pub fn state_aliases_mut(&mut self) -> &mut HashMap<A, A> {
105 &mut self.state_aliases
106 }
107
108 pub fn is_empty(&self) -> bool {
109 self.state_aliases.is_empty()
110 }
111
112 pub fn clear(&mut self) {
113 self.state_aliases.clear();
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct LayoutInput<A> {
124 pub state_objects: Vec<StateObjectLayout<A>>,
125 pub working_addresses: Vec<A>,
126 pub sparse_addresses: Vec<A>,
127 pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
128 pub requirements: LayoutRequirements<A>,
129 pub ff_referenced_addresses: HashSet<A>,
130 pub num_events: usize,
131 pub runtime_event_sites: Vec<RuntimeEventSite>,
132}
133
134pub trait LayoutSource<A> {
136 fn layout_input(&self, mode: MemoryLayoutMode) -> LayoutInput<A>;
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(bound(
141 serialize = "A: Serialize + Eq + Hash",
142 deserialize = "A: Deserialize<'de> + Eq + Hash"
143))]
144pub struct MemoryLayout<A> {
145 pub trace: Option<TraceLayout>,
147 pub four_state: bool,
148 pub mode: MemoryLayoutMode,
149 pub offsets: HashMap<A, usize>,
151 pub widths: HashMap<A, usize>,
152 pub is_4states: HashMap<A, bool>,
154 pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
155 pub total_size: usize,
157
158 pub working_offsets: HashMap<A, usize>,
160 pub working_base_offset: usize,
161 pub sparse_offsets: HashMap<A, usize>,
163 pub sparse_base_offset: usize,
164 pub sparse_layouts: HashMap<A, SparseWorkingLayout>,
165 pub sparse_active_bits_offset: usize,
166 pub sparse_active_capacity: usize,
167 pub merged_total_size: usize,
168
169 pub triggered_bits_offset: usize,
170 pub triggered_bits_total_size: usize,
171
172 pub scratch_base_offset: usize,
173 pub scratch_size: usize,
174
175 pub runtime_event_capacity: usize,
176 pub runtime_event_slot_size: usize,
177 pub runtime_event_buffer_size: usize,
178 pub runtime_event_site_layouts: Vec<RuntimeEventSiteLayout>,
179}
180
181type PhysicalLayoutObject<A> = (A, usize, bool, usize, usize);
182
183fn sort_layout_objects<A: Copy + Ord>(objects: &mut [PhysicalLayoutObject<A>]) {
184 objects.sort_unstable_by_key(|(address, _, _, _, alignment)| {
188 (std::cmp::Reverse(*alignment), *address)
189 });
190}
191
192impl<A> MemoryLayout<A>
193where
194 A: Copy + Eq + Hash + Ord,
195{
196 pub fn build<S>(source: &S, four_state: bool, mode: MemoryLayoutMode) -> Self
197 where
198 S: LayoutSource<A>,
199 {
200 let input = source.layout_input(mode);
201 let LayoutInput {
202 state_objects,
203 working_addresses,
204 sparse_addresses,
205 unpacked_arrays,
206 requirements,
207 ff_referenced_addresses,
208 num_events,
209 runtime_event_sites,
210 } = input;
211
212 let mut stable_objects = state_objects
213 .into_iter()
214 .map(|object| {
215 let size = unpacked_arrays
216 .get(&object.address)
217 .map(|layout| layout.plane_size)
218 .unwrap_or_else(|| get_byte_size(object.width));
219 let alignment = unpacked_arrays
220 .get(&object.address)
221 .map(|layout| layout.element_stride.min(8))
222 .unwrap_or_else(|| get_alignment(object.width));
223 (
224 object.address,
225 object.width,
226 object.is_4state,
227 size,
228 alignment,
229 )
230 })
231 .collect::<Vec<_>>();
232 sort_layout_objects(&mut stable_objects);
233
234 let mut offsets = HashMap::default();
235 let mut widths = HashMap::default();
236 let mut is_4states = HashMap::default();
237 let runtime_event_site_layouts = build_runtime_event_site_layouts(&runtime_event_sites);
238 let runtime_event_slot_size = RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
239 + runtime_event_site_layouts
240 .iter()
241 .map(|site| site.payload_words)
242 .max()
243 .unwrap_or(0)
244 * 8;
245
246 let mut current_offset = STATE_HEADER_SIZE;
247 for (address, width, is_4state, size, alignment) in stable_objects {
248 current_offset = align_up(current_offset, alignment);
249 offsets.insert(address, current_offset);
250 widths.insert(address, width);
251 is_4states.insert(address, is_4state);
252 current_offset += size;
253 if four_state {
254 current_offset += size;
255 }
256 }
257
258 let mut working_objects = working_addresses
259 .iter()
260 .map(|address| {
261 let width = widths[address];
262 let size = unpacked_arrays
263 .get(address)
264 .map(|layout| layout.plane_size)
265 .unwrap_or_else(|| get_byte_size(width));
266 let alignment = unpacked_arrays
267 .get(address)
268 .map(|layout| layout.element_stride.min(8))
269 .unwrap_or_else(|| get_alignment(width));
270 (*address, width, is_4states[address], size, alignment)
271 })
272 .collect::<Vec<_>>();
273 sort_layout_objects(&mut working_objects);
274
275 let mut working_offsets = HashMap::default();
276 let mut working_size = 0;
277 for (address, _, _, size, alignment) in working_objects {
278 working_size = align_up(working_size, alignment);
279 working_offsets.insert(address, working_size);
280 working_size += size;
281 if four_state {
282 working_size += size;
283 }
284 }
285
286 let mut sparse_objects = sparse_addresses
287 .iter()
288 .map(|address| {
289 let width = widths[address];
290 let size = unpacked_arrays
291 .get(address)
292 .map(|layout| layout.plane_size)
293 .unwrap_or_else(|| get_byte_size(width));
294 let alignment = unpacked_arrays
295 .get(address)
296 .map(|layout| layout.element_stride.min(8))
297 .unwrap_or_else(|| get_alignment(width));
298 (*address, width, is_4states[address], size, alignment)
299 })
300 .collect::<Vec<_>>();
301 sort_layout_objects(&mut sparse_objects);
302
303 let mut sparse_offsets = HashMap::default();
304 let mut sparse_size = 0usize;
305 for (address, _, _, size, alignment) in sparse_objects {
306 sparse_size = align_up(sparse_size, alignment);
307 sparse_offsets.insert(address, sparse_size);
308 let plane_count = if four_state { 2 } else { 1 };
309 let final_chunk_size = align_up(size, 8);
310 let physical_extent = (plane_count - 1) * size + final_chunk_size;
311 sparse_size += align_up(physical_extent, 8);
312 }
313
314 let working_base_offset = align_up(current_offset, 8);
315 let sparse_base_offset = align_up(working_base_offset + working_size, 8);
316 let mut sparse_metadata_offset = align_up(sparse_base_offset + sparse_size, 8);
317 let mut sparse_layouts = HashMap::default();
318 let mut sparse_order = sparse_addresses;
319 sparse_order.sort_unstable();
320 let sparse_active_capacity = sparse_order.len();
321 for (active_index, address) in sparse_order.into_iter().enumerate() {
322 let chunk_count = unpacked_arrays
323 .get(&address)
324 .map(|layout| layout.plane_size.div_ceil(8))
325 .unwrap_or_else(|| widths[&address].div_ceil(64));
326 let dirty_word_count = chunk_count.div_ceil(64);
327 let summary_word_count = dirty_word_count.div_ceil(64);
328 let dirty_words_offset = sparse_metadata_offset;
329 sparse_metadata_offset += dirty_word_count * 8;
330 let summary_words_offset = sparse_metadata_offset;
331 sparse_metadata_offset += summary_word_count * 8;
332 sparse_layouts.insert(
333 address,
334 SparseWorkingLayout {
335 active_index,
336 chunk_count,
337 dirty_words_offset,
338 dirty_word_count,
339 summary_words_offset,
340 summary_word_count,
341 },
342 );
343 }
344
345 let sparse_active_bits_offset = align_up(sparse_metadata_offset, 8);
346 sparse_metadata_offset =
347 sparse_active_bits_offset + sparse_active_capacity.div_ceil(64) * 8;
348 let triggered_bits_offset = align_up(sparse_metadata_offset, 8);
349 let triggered_bits_total_size = num_events.div_ceil(8);
350 let scratch_base_offset = align_up(triggered_bits_offset + triggered_bits_total_size, 8);
351 let runtime_event_buffer_size =
352 RUNTIME_EVENT_HEADER_SIZE + RUNTIME_EVENT_CAPACITY * runtime_event_slot_size;
353 let merged_total_size = scratch_base_offset;
354
355 let mut address_aliases = requirements.state_aliases.into_iter().collect::<Vec<_>>();
356 address_aliases.sort_unstable();
357 for (alias, canonical) in address_aliases {
358 let fourstate_ok = !four_state
359 || (is_4states.get(&alias) == Some(&false)
360 && is_4states.get(&canonical) == Some(&false));
361 let alias_fits = widths
362 .get(&alias)
363 .zip(widths.get(&canonical))
364 .is_some_and(|(&alias_width, &canonical_width)| alias_width <= canonical_width);
365 if fourstate_ok
366 && alias_fits
367 && !ff_referenced_addresses.contains(&alias)
368 && let Some(&canonical_offset) = offsets.get(&canonical)
369 {
370 offsets.insert(alias, canonical_offset);
371 }
372 }
373
374 Self {
375 trace: None,
376 four_state,
377 mode,
378 offsets,
379 widths,
380 is_4states,
381 unpacked_arrays,
382 total_size: current_offset,
383 working_offsets,
384 working_base_offset,
385 sparse_offsets,
386 sparse_base_offset,
387 sparse_layouts,
388 sparse_active_bits_offset,
389 sparse_active_capacity,
390 merged_total_size,
391 triggered_bits_offset,
392 triggered_bits_total_size,
393 scratch_base_offset,
394 scratch_size: 0,
395 runtime_event_capacity: RUNTIME_EVENT_CAPACITY,
396 runtime_event_slot_size,
397 runtime_event_buffer_size,
398 runtime_event_site_layouts,
399 }
400 }
401
402 pub fn with_backend_scratch(mut self, scratch_size: usize) -> Self {
406 self.scratch_size = scratch_size;
407 self.merged_total_size = align_up(self.scratch_base_offset + scratch_size, 8);
408 self
409 }
410
411 pub fn enable_trace(&mut self) {
414 if self.trace.is_some() {
415 return;
416 }
417 assert_eq!(
418 self.scratch_size, 0,
419 "trace must be enabled before backend planning"
420 );
421 let homes = self
422 .offsets
423 .iter()
424 .map(|(address, &offset)| {
425 let planes = if self.four_state { 2 } else { 1 };
426 (offset, offset + self.plane_size(address) * planes)
427 })
428 .collect();
429 let trace = TraceLayout::new(self.scratch_base_offset, self.total_size, homes);
430 self.scratch_base_offset = align_up(trace.end_offset(), 8);
431 self.merged_total_size = self.scratch_base_offset;
432 self.trace = Some(trace);
433 }
434
435 pub fn trace_notification_offsets<R: RegionedAddress<A>>(
436 &self,
437 address: &R,
438 ) -> Option<[usize; 2]> {
439 if address.region() != STABLE_REGION {
440 return None;
441 }
442 let trace = self.trace.as_ref()?;
443 Some(trace.notification_offsets(self.offsets[&address.absolute_address()]))
444 }
445
446 pub fn plane_size(&self, address: &A) -> usize {
447 self.unpacked_arrays
448 .get(address)
449 .map(|layout| layout.plane_size)
450 .unwrap_or_else(|| get_byte_size(self.widths[address]))
451 }
452
453 pub fn region_base_offset<R>(&self, address: &R) -> usize
454 where
455 R: RegionedAddress<A>,
456 {
457 let absolute = address.absolute_address();
458 match address.region() {
459 STABLE_REGION => self.offsets[&absolute],
460 SPARSE_WORKING_REGION => self.sparse_base_offset + self.sparse_offsets[&absolute],
461 _ => self.working_base_offset + self.working_offsets[&absolute],
462 }
463 }
464
465 pub fn map_static_bit_offset(&self, address: &A, bit_offset: usize) -> (usize, usize) {
466 let Some(array) = self.unpacked_arrays.get(address) else {
467 return (bit_offset / 8, bit_offset % 8);
468 };
469 let element = bit_offset / array.element_width;
470 let intra_element = bit_offset % array.element_width;
471 (
472 element * array.element_stride + intra_element / 8,
473 intra_element % 8,
474 )
475 }
476
477 pub fn regioned_static_byte_and_intra<R>(
478 &self,
479 address: &R,
480 bit_offset: usize,
481 ) -> Option<(i32, usize)>
482 where
483 R: RegionedAddress<A>,
484 {
485 let absolute = address.absolute_address();
486 let base = match address.region() {
487 STABLE_REGION => *self.offsets.get(&absolute).unwrap_or(&0),
488 SPARSE_WORKING_REGION => {
489 self.sparse_base_offset + *self.sparse_offsets.get(&absolute).unwrap_or(&0)
490 }
491 _ => self.working_base_offset + *self.working_offsets.get(&absolute).unwrap_or(&0),
492 };
493 let (byte, intra) = self.map_static_bit_offset(&absolute, bit_offset);
494 Some((i32::try_from(base.checked_add(byte)?).ok()?, intra))
495 }
496}
497
498pub trait RegionedAddress<A> {
499 fn region(&self) -> u32;
500 fn absolute_address(&self) -> A;
501}
502
503impl<V: Copy> RegionedAddress<AbsoluteAddrBase<V>> for RegionedAbsoluteAddrBase<V> {
504 fn region(&self) -> u32 {
505 self.region
506 }
507
508 fn absolute_address(&self) -> AbsoluteAddrBase<V> {
509 self.absolute_addr()
510 }
511}
512
513fn build_runtime_event_site_layouts(sites: &[RuntimeEventSite]) -> Vec<RuntimeEventSiteLayout> {
514 sites
515 .iter()
516 .map(|site| {
517 let mut payload_words = 0;
518 let args = site
519 .arg_widths
520 .iter()
521 .map(|width| {
522 let word_count = (*width).div_ceil(64).max(1);
523 let value_word_offset = payload_words;
524 payload_words += word_count;
525 let mask_word_offset = payload_words;
526 payload_words += word_count;
527 RuntimeEventArgLayout {
528 value_word_offset,
529 mask_word_offset,
530 word_count,
531 }
532 })
533 .collect();
534 RuntimeEventSiteLayout {
535 args,
536 payload_words,
537 }
538 })
539 .collect()
540}
541
542const fn align_up(offset: usize, alignment: usize) -> usize {
543 (offset + alignment - 1) & !(alignment - 1)
544}
545
546fn get_alignment(width: usize) -> usize {
547 let size = get_byte_size(width);
548 if size == 0 {
549 1
550 } else if size <= 8 {
551 size.next_power_of_two()
552 } else {
553 8
554 }
555}
556
557pub const fn get_byte_size(width: usize) -> usize {
558 width.div_ceil(8)
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn byte_size_rounds_up_partial_bytes() {
567 assert_eq!(get_byte_size(0), 0);
568 assert_eq!(get_byte_size(1), 1);
569 assert_eq!(get_byte_size(8), 1);
570 assert_eq!(get_byte_size(9), 2);
571 }
572
573 #[test]
574 fn layout_order_is_independent_of_input_iteration_order() {
575 let high_address = (3u32, 64, false, 8, 8);
576 let low_address = (1u32, 64, false, 8, 8);
577 let less_aligned = (0u32, 32, false, 4, 4);
578 let mut forward = vec![high_address, less_aligned, low_address];
579 let mut reverse = forward.iter().copied().rev().collect::<Vec<_>>();
580
581 sort_layout_objects(&mut forward);
582 sort_layout_objects(&mut reverse);
583
584 assert_eq!(forward, reverse);
585 assert_eq!(forward, vec![low_address, high_address, less_aligned]);
586 }
587
588 #[test]
589 fn layout_requirements_own_semantic_aliases_until_layout() {
590 let mut requirements = LayoutRequirements::default();
591 requirements.state_aliases_mut().insert(2u32, 1u32);
592
593 assert_eq!(requirements.state_aliases().get(&2), Some(&1));
594 assert!(!requirements.is_empty());
595
596 requirements.clear();
597 assert!(requirements.is_empty());
598 }
599
600 #[test]
601 fn layout_applies_aliases_from_requirements() {
602 struct AliasLayoutSource;
603
604 impl LayoutSource<u32> for AliasLayoutSource {
605 fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
606 let mut requirements = LayoutRequirements::default();
607 requirements.state_aliases_mut().insert(2, 1);
608 LayoutInput {
609 state_objects: vec![
610 StateObjectLayout {
611 address: 1,
612 width: 8,
613 is_4state: false,
614 },
615 StateObjectLayout {
616 address: 2,
617 width: 8,
618 is_4state: false,
619 },
620 ],
621 working_addresses: Vec::new(),
622 sparse_addresses: Vec::new(),
623 unpacked_arrays: HashMap::default(),
624 requirements,
625 ff_referenced_addresses: HashSet::default(),
626 num_events: 0,
627 runtime_event_sites: Vec::new(),
628 }
629 }
630 }
631
632 let layout = MemoryLayout::build(&AliasLayoutSource, false, MemoryLayoutMode::Packed);
633 assert_eq!(layout.offsets[&1], layout.offsets[&2]);
634 }
635
636 #[test]
637 fn backend_scratch_only_extends_the_final_layout_region() {
638 struct EmptyLayoutSource;
639
640 impl LayoutSource<u32> for EmptyLayoutSource {
641 fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
642 LayoutInput {
643 state_objects: Vec::new(),
644 working_addresses: Vec::new(),
645 sparse_addresses: Vec::new(),
646 unpacked_arrays: HashMap::default(),
647 requirements: LayoutRequirements::default(),
648 ff_referenced_addresses: HashSet::default(),
649 num_events: 3,
650 runtime_event_sites: Vec::new(),
651 }
652 }
653 }
654
655 let base = MemoryLayout::build(&EmptyLayoutSource, false, MemoryLayoutMode::Packed);
656 let expanded = base.clone().with_backend_scratch(13);
657
658 assert_eq!(base.scratch_size, 0);
659 assert_eq!(expanded.scratch_base_offset, base.scratch_base_offset);
660 assert_eq!(expanded.scratch_size, 13);
661 assert_eq!(expanded.merged_total_size, base.scratch_base_offset + 16);
662 assert_eq!(expanded.offsets, base.offsets);
663 assert_eq!(expanded.working_offsets, base.working_offsets);
664 assert_eq!(expanded.triggered_bits_offset, base.triggered_bits_offset);
665 }
666
667 #[test]
668 #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
669 fn state_header_fields_do_not_overlap() {
670 const {
671 assert!(
672 STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET + 8
673 <= STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET
674 );
675 assert!(
676 STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET + 8
677 <= STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET
678 );
679 assert!(
680 STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET + 8
681 <= STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET
682 );
683 assert!(STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET + 8 <= STATE_HEADER_SIZE);
684 }
685 }
686}