Skip to main content

celox_state_layout/
lib.rs

1//! Shared physical simulation-state layout contracts.
2//!
3//! These types and offsets form the ABI between layout construction, generated
4//! code, and runtime state access. They contain no frontend or backend IR.
5
6use 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
14pub const RUNTIME_EVENT_CAPACITY: usize = 1024;
15pub const RUNTIME_EVENT_WRITING: u64 = u64::MAX;
16pub const STATE_HEADER_SIZE: usize = 32;
17pub const STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET: usize = 0;
18/// Remaining iterations for an in-function native tick loop.
19#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
20pub const STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET: usize = 8;
21#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
22pub const STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET: usize = 24;
23#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
24pub const STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET: usize = 16;
25/// Runtime-event write sequence observed when a native tick batch starts.
26pub const RUNTIME_EVENT_HEADER_SIZE: usize = 8;
27pub const RUNTIME_EVENT_SLOT_SEQ_OFFSET: usize = 0;
28pub const RUNTIME_EVENT_SLOT_SITE_OFFSET: usize = 8;
29pub const RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET: usize = 16;
30pub const RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET: usize = 24;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RuntimeEventArgLayout {
34    pub value_word_offset: usize,
35    pub mask_word_offset: usize,
36    pub word_count: usize,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RuntimeEventSiteLayout {
41    pub args: Vec<RuntimeEventArgLayout>,
42    pub payload_words: usize,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SparseWorkingLayout {
47    pub active_index: usize,
48    pub chunk_count: usize,
49    pub dirty_words_offset: usize,
50    pub dirty_word_count: usize,
51    pub summary_words_offset: usize,
52    pub summary_word_count: usize,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub enum MemoryLayoutMode {
57    Packed,
58    ElementStrided,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub struct UnpackedArrayLayout {
63    pub element_width: usize,
64    pub element_count: usize,
65    pub element_stride: usize,
66    pub plane_size: usize,
67}
68
69/// One semantic state object that requires stable storage.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct StateObjectLayout<A> {
72    pub address: A,
73    pub width: usize,
74    pub is_4state: bool,
75}
76
77/// Semantic constraints produced by optimization and consumed when physical
78/// state layout is finalized.
79///
80/// This deliberately contains no physical offsets.  Alias pairs state that
81/// two semantic objects may share one stable-state home; layout construction
82/// still validates representation compatibility before applying the pair.
83#[derive(Debug, Clone)]
84pub struct LayoutRequirements<A> {
85    state_aliases: HashMap<A, A>,
86}
87
88impl<A> Default for LayoutRequirements<A> {
89    fn default() -> Self {
90        Self {
91            state_aliases: HashMap::default(),
92        }
93    }
94}
95
96impl<A> LayoutRequirements<A> {
97    pub fn state_aliases(&self) -> &HashMap<A, A> {
98        &self.state_aliases
99    }
100
101    pub fn state_aliases_mut(&mut self) -> &mut HashMap<A, A> {
102        &mut self.state_aliases
103    }
104
105    pub fn is_empty(&self) -> bool {
106        self.state_aliases.is_empty()
107    }
108
109    pub fn clear(&mut self) {
110        self.state_aliases.clear();
111    }
112}
113
114/// Complete, backend-independent input to physical layout construction.
115///
116/// The compiler facade adapts its phase artifacts into this value. Layout
117/// construction therefore never needs to inspect frontend IR, optimizer plans,
118/// or a mixed compiler `Program`.
119#[derive(Debug, Clone)]
120pub struct LayoutInput<A> {
121    pub state_objects: Vec<StateObjectLayout<A>>,
122    pub working_addresses: Vec<A>,
123    pub sparse_addresses: Vec<A>,
124    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
125    pub requirements: LayoutRequirements<A>,
126    pub ff_referenced_addresses: HashSet<A>,
127    pub num_events: usize,
128    pub runtime_event_sites: Vec<RuntimeEventSite>,
129}
130
131/// Adapter implemented by the phase artifact that precedes physical layout.
132pub trait LayoutSource<A> {
133    fn layout_input(&self, mode: MemoryLayoutMode) -> LayoutInput<A>;
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(bound(
138    serialize = "A: Serialize + Eq + Hash",
139    deserialize = "A: Deserialize<'de> + Eq + Hash"
140))]
141pub struct MemoryLayout<A> {
142    pub four_state: bool,
143    pub mode: MemoryLayoutMode,
144    /// Stable region offsets. Includes all declared state objects.
145    pub offsets: HashMap<A, usize>,
146    pub widths: HashMap<A, usize>,
147    /// Whether each state object has a four-state source type.
148    pub is_4states: HashMap<A, bool>,
149    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
150    /// Stable region size in bytes.
151    pub total_size: usize,
152
153    /// Working region offsets. Includes only actually-used state objects.
154    pub working_offsets: HashMap<A, usize>,
155    pub working_base_offset: usize,
156    /// Copy-on-write next-state data for dynamically addressed FF targets.
157    pub sparse_offsets: HashMap<A, usize>,
158    pub sparse_base_offset: usize,
159    pub sparse_layouts: HashMap<A, SparseWorkingLayout>,
160    pub sparse_active_bits_offset: usize,
161    pub sparse_active_capacity: usize,
162    pub merged_total_size: usize,
163
164    pub triggered_bits_offset: usize,
165    pub triggered_bits_total_size: usize,
166
167    pub scratch_base_offset: usize,
168    pub scratch_size: usize,
169
170    pub runtime_event_capacity: usize,
171    pub runtime_event_slot_size: usize,
172    pub runtime_event_buffer_size: usize,
173    pub runtime_event_site_layouts: Vec<RuntimeEventSiteLayout>,
174}
175
176type PhysicalLayoutObject<A> = (A, usize, bool, usize, usize);
177
178fn sort_layout_objects<A: Copy + Ord>(objects: &mut [PhysicalLayoutObject<A>]) {
179    // Packing by decreasing alignment avoids padding. Equal-alignment objects
180    // use semantic-address order so randomized input maps cannot perturb every
181    // physical offset and the generated machine code that embeds it.
182    objects.sort_unstable_by_key(|(address, _, _, _, alignment)| {
183        (std::cmp::Reverse(*alignment), *address)
184    });
185}
186
187impl<A> MemoryLayout<A>
188where
189    A: Copy + Eq + Hash + Ord,
190{
191    pub fn build<S>(source: &S, four_state: bool, mode: MemoryLayoutMode) -> Self
192    where
193        S: LayoutSource<A>,
194    {
195        let input = source.layout_input(mode);
196        let LayoutInput {
197            state_objects,
198            working_addresses,
199            sparse_addresses,
200            unpacked_arrays,
201            requirements,
202            ff_referenced_addresses,
203            num_events,
204            runtime_event_sites,
205        } = input;
206
207        let mut stable_objects = state_objects
208            .into_iter()
209            .map(|object| {
210                let size = unpacked_arrays
211                    .get(&object.address)
212                    .map(|layout| layout.plane_size)
213                    .unwrap_or_else(|| get_byte_size(object.width));
214                let alignment = unpacked_arrays
215                    .get(&object.address)
216                    .map(|layout| layout.element_stride.min(8))
217                    .unwrap_or_else(|| get_alignment(object.width));
218                (
219                    object.address,
220                    object.width,
221                    object.is_4state,
222                    size,
223                    alignment,
224                )
225            })
226            .collect::<Vec<_>>();
227        sort_layout_objects(&mut stable_objects);
228
229        let mut offsets = HashMap::default();
230        let mut widths = HashMap::default();
231        let mut is_4states = HashMap::default();
232        let runtime_event_site_layouts = build_runtime_event_site_layouts(&runtime_event_sites);
233        let runtime_event_slot_size = RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
234            + runtime_event_site_layouts
235                .iter()
236                .map(|site| site.payload_words)
237                .max()
238                .unwrap_or(0)
239                * 8;
240
241        let mut current_offset = STATE_HEADER_SIZE;
242        for (address, width, is_4state, size, alignment) in stable_objects {
243            current_offset = align_up(current_offset, alignment);
244            offsets.insert(address, current_offset);
245            widths.insert(address, width);
246            is_4states.insert(address, is_4state);
247            current_offset += size;
248            if four_state {
249                current_offset += size;
250            }
251        }
252
253        let mut working_objects = working_addresses
254            .iter()
255            .map(|address| {
256                let width = widths[address];
257                let size = unpacked_arrays
258                    .get(address)
259                    .map(|layout| layout.plane_size)
260                    .unwrap_or_else(|| get_byte_size(width));
261                let alignment = unpacked_arrays
262                    .get(address)
263                    .map(|layout| layout.element_stride.min(8))
264                    .unwrap_or_else(|| get_alignment(width));
265                (*address, width, is_4states[address], size, alignment)
266            })
267            .collect::<Vec<_>>();
268        sort_layout_objects(&mut working_objects);
269
270        let mut working_offsets = HashMap::default();
271        let mut working_size = 0;
272        for (address, _, _, size, alignment) in working_objects {
273            working_size = align_up(working_size, alignment);
274            working_offsets.insert(address, working_size);
275            working_size += size;
276            if four_state {
277                working_size += size;
278            }
279        }
280
281        let mut sparse_objects = sparse_addresses
282            .iter()
283            .map(|address| {
284                let width = widths[address];
285                let size = unpacked_arrays
286                    .get(address)
287                    .map(|layout| layout.plane_size)
288                    .unwrap_or_else(|| get_byte_size(width));
289                let alignment = unpacked_arrays
290                    .get(address)
291                    .map(|layout| layout.element_stride.min(8))
292                    .unwrap_or_else(|| get_alignment(width));
293                (*address, width, is_4states[address], size, alignment)
294            })
295            .collect::<Vec<_>>();
296        sort_layout_objects(&mut sparse_objects);
297
298        let mut sparse_offsets = HashMap::default();
299        let mut sparse_size = 0usize;
300        for (address, _, _, size, alignment) in sparse_objects {
301            sparse_size = align_up(sparse_size, alignment);
302            sparse_offsets.insert(address, sparse_size);
303            let plane_count = if four_state { 2 } else { 1 };
304            let final_chunk_size = align_up(size, 8);
305            let physical_extent = (plane_count - 1) * size + final_chunk_size;
306            sparse_size += align_up(physical_extent, 8);
307        }
308
309        let working_base_offset = align_up(current_offset, 8);
310        let sparse_base_offset = align_up(working_base_offset + working_size, 8);
311        let mut sparse_metadata_offset = align_up(sparse_base_offset + sparse_size, 8);
312        let mut sparse_layouts = HashMap::default();
313        let mut sparse_order = sparse_addresses;
314        sparse_order.sort_unstable();
315        let sparse_active_capacity = sparse_order.len();
316        for (active_index, address) in sparse_order.into_iter().enumerate() {
317            let chunk_count = unpacked_arrays
318                .get(&address)
319                .map(|layout| layout.plane_size.div_ceil(8))
320                .unwrap_or_else(|| widths[&address].div_ceil(64));
321            let dirty_word_count = chunk_count.div_ceil(64);
322            let summary_word_count = dirty_word_count.div_ceil(64);
323            let dirty_words_offset = sparse_metadata_offset;
324            sparse_metadata_offset += dirty_word_count * 8;
325            let summary_words_offset = sparse_metadata_offset;
326            sparse_metadata_offset += summary_word_count * 8;
327            sparse_layouts.insert(
328                address,
329                SparseWorkingLayout {
330                    active_index,
331                    chunk_count,
332                    dirty_words_offset,
333                    dirty_word_count,
334                    summary_words_offset,
335                    summary_word_count,
336                },
337            );
338        }
339
340        let sparse_active_bits_offset = align_up(sparse_metadata_offset, 8);
341        sparse_metadata_offset =
342            sparse_active_bits_offset + sparse_active_capacity.div_ceil(64) * 8;
343        let triggered_bits_offset = align_up(sparse_metadata_offset, 8);
344        let triggered_bits_total_size = num_events.div_ceil(8);
345        let scratch_base_offset = align_up(triggered_bits_offset + triggered_bits_total_size, 8);
346        let runtime_event_buffer_size =
347            RUNTIME_EVENT_HEADER_SIZE + RUNTIME_EVENT_CAPACITY * runtime_event_slot_size;
348        let merged_total_size = scratch_base_offset;
349
350        let mut address_aliases = requirements.state_aliases.into_iter().collect::<Vec<_>>();
351        address_aliases.sort_unstable();
352        for (alias, canonical) in address_aliases {
353            let fourstate_ok = !four_state
354                || (is_4states.get(&alias) == Some(&false)
355                    && is_4states.get(&canonical) == Some(&false));
356            let alias_fits = widths
357                .get(&alias)
358                .zip(widths.get(&canonical))
359                .is_some_and(|(&alias_width, &canonical_width)| alias_width <= canonical_width);
360            if fourstate_ok
361                && alias_fits
362                && !ff_referenced_addresses.contains(&alias)
363                && let Some(&canonical_offset) = offsets.get(&canonical)
364            {
365                offsets.insert(alias, canonical_offset);
366            }
367        }
368
369        Self {
370            four_state,
371            mode,
372            offsets,
373            widths,
374            is_4states,
375            unpacked_arrays,
376            total_size: current_offset,
377            working_offsets,
378            working_base_offset,
379            sparse_offsets,
380            sparse_base_offset,
381            sparse_layouts,
382            sparse_active_bits_offset,
383            sparse_active_capacity,
384            merged_total_size,
385            triggered_bits_offset,
386            triggered_bits_total_size,
387            scratch_base_offset,
388            scratch_size: 0,
389            runtime_event_capacity: RUNTIME_EVENT_CAPACITY,
390            runtime_event_slot_size,
391            runtime_event_buffer_size,
392            runtime_event_site_layouts,
393        }
394    }
395
396    /// Append backend-private scratch storage without changing any semantic
397    /// state offset. Backend planning happens after the backend-neutral state
398    /// layout has been finalized, so scratch is always the final region.
399    pub fn with_backend_scratch(mut self, scratch_size: usize) -> Self {
400        self.scratch_size = scratch_size;
401        self.merged_total_size = align_up(self.scratch_base_offset + scratch_size, 8);
402        self
403    }
404
405    pub fn plane_size(&self, address: &A) -> usize {
406        self.unpacked_arrays
407            .get(address)
408            .map(|layout| layout.plane_size)
409            .unwrap_or_else(|| get_byte_size(self.widths[address]))
410    }
411
412    pub fn region_base_offset<R>(&self, address: &R) -> usize
413    where
414        R: RegionedAddress<A>,
415    {
416        let absolute = address.absolute_address();
417        match address.region() {
418            STABLE_REGION => self.offsets[&absolute],
419            SPARSE_WORKING_REGION => self.sparse_base_offset + self.sparse_offsets[&absolute],
420            _ => self.working_base_offset + self.working_offsets[&absolute],
421        }
422    }
423
424    pub fn map_static_bit_offset(&self, address: &A, bit_offset: usize) -> (usize, usize) {
425        let Some(array) = self.unpacked_arrays.get(address) else {
426            return (bit_offset / 8, bit_offset % 8);
427        };
428        let element = bit_offset / array.element_width;
429        let intra_element = bit_offset % array.element_width;
430        (
431            element * array.element_stride + intra_element / 8,
432            intra_element % 8,
433        )
434    }
435
436    pub fn regioned_static_byte_and_intra<R>(
437        &self,
438        address: &R,
439        bit_offset: usize,
440    ) -> Option<(i32, usize)>
441    where
442        R: RegionedAddress<A>,
443    {
444        let absolute = address.absolute_address();
445        let base = match address.region() {
446            STABLE_REGION => *self.offsets.get(&absolute).unwrap_or(&0),
447            SPARSE_WORKING_REGION => {
448                self.sparse_base_offset + *self.sparse_offsets.get(&absolute).unwrap_or(&0)
449            }
450            _ => self.working_base_offset + *self.working_offsets.get(&absolute).unwrap_or(&0),
451        };
452        let (byte, intra) = self.map_static_bit_offset(&absolute, bit_offset);
453        Some((i32::try_from(base.checked_add(byte)?).ok()?, intra))
454    }
455}
456
457pub trait RegionedAddress<A> {
458    fn region(&self) -> u32;
459    fn absolute_address(&self) -> A;
460}
461
462impl<V: Copy> RegionedAddress<AbsoluteAddrBase<V>> for RegionedAbsoluteAddrBase<V> {
463    fn region(&self) -> u32 {
464        self.region
465    }
466
467    fn absolute_address(&self) -> AbsoluteAddrBase<V> {
468        self.absolute_addr()
469    }
470}
471
472fn build_runtime_event_site_layouts(sites: &[RuntimeEventSite]) -> Vec<RuntimeEventSiteLayout> {
473    sites
474        .iter()
475        .map(|site| {
476            let mut payload_words = 0;
477            let args = site
478                .arg_widths
479                .iter()
480                .map(|width| {
481                    let word_count = (*width).div_ceil(64).max(1);
482                    let value_word_offset = payload_words;
483                    payload_words += word_count;
484                    let mask_word_offset = payload_words;
485                    payload_words += word_count;
486                    RuntimeEventArgLayout {
487                        value_word_offset,
488                        mask_word_offset,
489                        word_count,
490                    }
491                })
492                .collect();
493            RuntimeEventSiteLayout {
494                args,
495                payload_words,
496            }
497        })
498        .collect()
499}
500
501const fn align_up(offset: usize, alignment: usize) -> usize {
502    (offset + alignment - 1) & !(alignment - 1)
503}
504
505fn get_alignment(width: usize) -> usize {
506    let size = get_byte_size(width);
507    if size == 0 {
508        1
509    } else if size <= 8 {
510        size.next_power_of_two()
511    } else {
512        8
513    }
514}
515
516pub const fn get_byte_size(width: usize) -> usize {
517    width.div_ceil(8)
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[test]
525    fn byte_size_rounds_up_partial_bytes() {
526        assert_eq!(get_byte_size(0), 0);
527        assert_eq!(get_byte_size(1), 1);
528        assert_eq!(get_byte_size(8), 1);
529        assert_eq!(get_byte_size(9), 2);
530    }
531
532    #[test]
533    fn layout_order_is_independent_of_input_iteration_order() {
534        let high_address = (3u32, 64, false, 8, 8);
535        let low_address = (1u32, 64, false, 8, 8);
536        let less_aligned = (0u32, 32, false, 4, 4);
537        let mut forward = vec![high_address, less_aligned, low_address];
538        let mut reverse = forward.iter().copied().rev().collect::<Vec<_>>();
539
540        sort_layout_objects(&mut forward);
541        sort_layout_objects(&mut reverse);
542
543        assert_eq!(forward, reverse);
544        assert_eq!(forward, vec![low_address, high_address, less_aligned]);
545    }
546
547    #[test]
548    fn layout_requirements_own_semantic_aliases_until_layout() {
549        let mut requirements = LayoutRequirements::default();
550        requirements.state_aliases_mut().insert(2u32, 1u32);
551
552        assert_eq!(requirements.state_aliases().get(&2), Some(&1));
553        assert!(!requirements.is_empty());
554
555        requirements.clear();
556        assert!(requirements.is_empty());
557    }
558
559    #[test]
560    fn layout_applies_aliases_from_requirements() {
561        struct AliasLayoutSource;
562
563        impl LayoutSource<u32> for AliasLayoutSource {
564            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
565                let mut requirements = LayoutRequirements::default();
566                requirements.state_aliases_mut().insert(2, 1);
567                LayoutInput {
568                    state_objects: vec![
569                        StateObjectLayout {
570                            address: 1,
571                            width: 8,
572                            is_4state: false,
573                        },
574                        StateObjectLayout {
575                            address: 2,
576                            width: 8,
577                            is_4state: false,
578                        },
579                    ],
580                    working_addresses: Vec::new(),
581                    sparse_addresses: Vec::new(),
582                    unpacked_arrays: HashMap::default(),
583                    requirements,
584                    ff_referenced_addresses: HashSet::default(),
585                    num_events: 0,
586                    runtime_event_sites: Vec::new(),
587                }
588            }
589        }
590
591        let layout = MemoryLayout::build(&AliasLayoutSource, false, MemoryLayoutMode::Packed);
592        assert_eq!(layout.offsets[&1], layout.offsets[&2]);
593    }
594
595    #[test]
596    fn backend_scratch_only_extends_the_final_layout_region() {
597        struct EmptyLayoutSource;
598
599        impl LayoutSource<u32> for EmptyLayoutSource {
600            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
601                LayoutInput {
602                    state_objects: Vec::new(),
603                    working_addresses: Vec::new(),
604                    sparse_addresses: Vec::new(),
605                    unpacked_arrays: HashMap::default(),
606                    requirements: LayoutRequirements::default(),
607                    ff_referenced_addresses: HashSet::default(),
608                    num_events: 3,
609                    runtime_event_sites: Vec::new(),
610                }
611            }
612        }
613
614        let base = MemoryLayout::build(&EmptyLayoutSource, false, MemoryLayoutMode::Packed);
615        let expanded = base.clone().with_backend_scratch(13);
616
617        assert_eq!(base.scratch_size, 0);
618        assert_eq!(expanded.scratch_base_offset, base.scratch_base_offset);
619        assert_eq!(expanded.scratch_size, 13);
620        assert_eq!(expanded.merged_total_size, base.scratch_base_offset + 16);
621        assert_eq!(expanded.offsets, base.offsets);
622        assert_eq!(expanded.working_offsets, base.working_offsets);
623        assert_eq!(expanded.triggered_bits_offset, base.triggered_bits_offset);
624    }
625
626    #[test]
627    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
628    fn state_header_fields_do_not_overlap() {
629        const {
630            assert!(
631                STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET + 8
632                    <= STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET
633            );
634            assert!(
635                STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET + 8
636                    <= STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET
637            );
638            assert!(
639                STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET + 8
640                    <= STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET
641            );
642            assert!(STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET + 8 <= STATE_HEADER_SIZE);
643        }
644    }
645}