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
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 = 40;
20pub const STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET: usize = 0;
21/// Remaining iterations for an in-function native tick loop.
22#[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;
28/// Address of the table of extern function pointers called by `ExternCall`.
29pub const STATE_HEADER_EXTERN_FUNCTIONS_ADDR_OFFSET: usize = 32;
30/// Runtime-event write sequence observed when a native tick batch starts.
31pub const RUNTIME_EVENT_HEADER_SIZE: usize = 8;
32pub const RUNTIME_EVENT_SLOT_SEQ_OFFSET: usize = 0;
33pub const RUNTIME_EVENT_SLOT_SITE_OFFSET: usize = 8;
34pub const RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET: usize = 16;
35pub const RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET: usize = 24;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct RuntimeEventArgLayout {
39    pub value_word_offset: usize,
40    pub mask_word_offset: usize,
41    pub word_count: usize,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct RuntimeEventSiteLayout {
46    pub args: Vec<RuntimeEventArgLayout>,
47    pub payload_words: usize,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct SparseWorkingLayout {
52    pub active_index: usize,
53    pub chunk_count: usize,
54    pub dirty_words_offset: usize,
55    pub dirty_word_count: usize,
56    pub summary_words_offset: usize,
57    pub summary_word_count: usize,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub enum MemoryLayoutMode {
62    Packed,
63    ElementStrided,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub struct UnpackedArrayLayout {
68    pub element_width: usize,
69    pub element_count: usize,
70    pub element_stride: usize,
71    pub plane_size: usize,
72}
73
74/// One semantic state object that requires stable storage.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct StateObjectLayout<A> {
77    pub address: A,
78    pub width: usize,
79    pub is_4state: bool,
80}
81
82/// Semantic constraints produced by optimization and consumed when physical
83/// state layout is finalized.
84///
85/// This deliberately contains no physical offsets.  Alias pairs state that
86/// two semantic objects may share one stable-state home; layout construction
87/// still validates representation compatibility before applying the pair.
88#[derive(Debug, Clone)]
89pub struct LayoutRequirements<A> {
90    state_aliases: HashMap<A, A>,
91}
92
93impl<A> Default for LayoutRequirements<A> {
94    fn default() -> Self {
95        Self {
96            state_aliases: HashMap::default(),
97        }
98    }
99}
100
101impl<A> LayoutRequirements<A> {
102    pub fn state_aliases(&self) -> &HashMap<A, A> {
103        &self.state_aliases
104    }
105
106    pub fn state_aliases_mut(&mut self) -> &mut HashMap<A, A> {
107        &mut self.state_aliases
108    }
109
110    pub fn is_empty(&self) -> bool {
111        self.state_aliases.is_empty()
112    }
113
114    pub fn clear(&mut self) {
115        self.state_aliases.clear();
116    }
117}
118
119/// Common coherence-isolation boundary, including CPUs with 256-byte lines.
120/// Smaller-line CPUs use the same layout; objects within a lane segment
121/// remain packed rather than each receiving this alignment.
122pub const LANE_SEGMENT_ALIGN: usize = 256;
123/// Minimum gap after a lane segment. Native code may widen a sub-word
124/// read-modify-write past the end of an object (up to eight bytes for a
125/// dynamic store plus its trailing byte); the gap keeps such accesses inside
126/// storage owned by the same lane.
127pub const LANE_SEGMENT_GUARD: usize = 16;
128
129/// Lanes that write each state home in lane-partitioned kernels, as a bit
130/// set per object. An absent object is not written by any partitioned unit.
131#[derive(Debug, Clone)]
132pub struct LaneWriters<A> {
133    pub stable: HashMap<A, u64>,
134    pub working: HashMap<A, u64>,
135    pub sparse: HashMap<A, u64>,
136}
137
138impl<A> Default for LaneWriters<A> {
139    fn default() -> Self {
140        Self {
141            stable: HashMap::default(),
142            working: HashMap::default(),
143            sparse: HashMap::default(),
144        }
145    }
146}
147
148impl<A> LaneWriters<A> {
149    pub fn is_empty(&self) -> bool {
150        self.stable.is_empty() && self.working.is_empty() && self.sparse.is_empty()
151    }
152}
153
154/// Complete, backend-independent input to physical layout construction.
155///
156/// The compiler facade adapts its phase artifacts into this value. Layout
157/// construction therefore never needs to inspect frontend IR, optimizer plans,
158/// or a mixed compiler `Program`.
159#[derive(Debug, Clone)]
160pub struct LayoutInput<A> {
161    pub state_objects: Vec<StateObjectLayout<A>>,
162    pub working_addresses: Vec<A>,
163    pub sparse_addresses: Vec<A>,
164    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
165    pub requirements: LayoutRequirements<A>,
166    pub ff_referenced_addresses: HashSet<A>,
167    pub num_events: usize,
168    pub runtime_event_sites: Vec<RuntimeEventSite>,
169    /// Writer lanes of state homes in lane-partitioned kernels. Homes written
170    /// by exactly one lane are placed in that lane's segment; homes written by
171    /// several lanes are isolated in their own segment.
172    pub lane_writers: LaneWriters<A>,
173    /// Rank of each state home in the order the per-tick code first accesses
174    /// it. Homes are placed in rank order so data used together shares cache
175    /// lines and is read in address order; unranked homes go last.
176    pub access_rank: HashMap<A, u32>,
177}
178
179/// Adapter implemented by the phase artifact that precedes physical layout.
180pub trait LayoutSource<A> {
181    fn layout_input(&self, mode: MemoryLayoutMode) -> LayoutInput<A>;
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[serde(bound(
186    serialize = "A: Serialize + Eq + Hash",
187    deserialize = "A: Deserialize<'de> + Eq + Hash"
188))]
189pub struct MemoryLayout<A> {
190    /// Present only in builds that record waveforms.
191    pub trace: Option<TraceLayout>,
192    pub four_state: bool,
193    pub mode: MemoryLayoutMode,
194    /// Stable region offsets. Includes all declared state objects.
195    pub offsets: HashMap<A, usize>,
196    pub widths: HashMap<A, usize>,
197    /// Whether each state object has a four-state source type.
198    pub is_4states: HashMap<A, bool>,
199    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
200    /// Stable region size in bytes.
201    pub total_size: usize,
202
203    /// Working region offsets. Includes only actually-used state objects.
204    pub working_offsets: HashMap<A, usize>,
205    pub working_base_offset: usize,
206    /// Copy-on-write next-state data for dynamically addressed FF targets.
207    pub sparse_offsets: HashMap<A, usize>,
208    pub sparse_base_offset: usize,
209    pub sparse_layouts: HashMap<A, SparseWorkingLayout>,
210    pub sparse_active_bits_offset: usize,
211    pub sparse_active_capacity: usize,
212    pub merged_total_size: usize,
213
214    pub triggered_bits_offset: usize,
215    pub triggered_bits_total_size: usize,
216
217    pub scratch_base_offset: usize,
218    pub scratch_size: usize,
219
220    pub runtime_event_capacity: usize,
221    pub runtime_event_slot_size: usize,
222    pub runtime_event_buffer_size: usize,
223    pub runtime_event_site_layouts: Vec<RuntimeEventSiteLayout>,
224}
225
226type PhysicalLayoutObject<A> = (A, usize, bool, usize, usize);
227
228/// Objects larger than this are placed after the smaller objects of their
229/// run, so they do not spread the small, frequently accessed homes across
230/// more cache lines.
231const LARGE_OBJECT_BYTES: usize = 256;
232
233/// How far behind the end of a run a padding hole may still be filled.
234/// Filling only nearby holes keeps objects close to their access-order
235/// neighbours.
236const HOLE_REACH: usize = 128;
237
238fn sort_layout_objects<A: Copy + Ord + Hash>(
239    objects: &mut [PhysicalLayoutObject<A>],
240    access_rank: &HashMap<A, u32>,
241) {
242    // Small accessed homes come first in access order, then large accessed
243    // homes in access order, then homes the per-tick code never touches.
244    // Unranked homes pack by decreasing alignment, which avoids padding.
245    // Ties use semantic-address order so randomized input maps cannot
246    // perturb every physical offset and the generated machine code that
247    // embeds it.
248    objects.sort_unstable_by_key(|&(address, _, _, size, alignment)| {
249        match access_rank.get(&address) {
250            Some(&rank) => (u8::from(size > LARGE_OBJECT_BYTES), rank, 0, address),
251            None => (2, 0, usize::MAX - alignment, address),
252        }
253    });
254}
255
256/// Place sorted objects of one region starting at `start`.
257///
258/// Objects written by no lane-partitioned unit are packed first. Every lane's
259/// objects then follow in their own segment, and every object written by more
260/// than one lane gets a segment of its own. A segment starts on a
261/// [`LANE_SEGMENT_ALIGN`] boundary at least [`LANE_SEGMENT_GUARD`] bytes after
262/// the previous one, and the region ends with the same gap, so no access
263/// widened past one lane's object reaches storage written by another lane.
264/// Without writer information this is the ordinary packed placement.
265///
266/// Within a run, objects keep their sorted order. An object that fits in the
267/// alignment padding left shortly before the end of the run fills it instead.
268fn place_region<A: Copy + Eq + Hash>(
269    objects: Vec<PhysicalLayoutObject<A>>,
270    writers: &HashMap<A, u64>,
271    start: usize,
272    mut extent: impl FnMut(&PhysicalLayoutObject<A>) -> usize,
273    mut place: impl FnMut(&PhysicalLayoutObject<A>, usize),
274) -> usize {
275    let mut common = Vec::new();
276    let mut lanes: Vec<Vec<PhysicalLayoutObject<A>>> = Vec::new();
277    let mut isolated = Vec::new();
278    for object in objects {
279        match writers.get(&object.0).copied().unwrap_or(0) {
280            0 => common.push(object),
281            mask if mask.is_power_of_two() => {
282                let lane = mask.trailing_zeros() as usize;
283                if lanes.len() <= lane {
284                    lanes.resize_with(lane + 1, Vec::new);
285                }
286                lanes[lane].push(object);
287            }
288            _ => isolated.push(object),
289        }
290    }
291    let mut current = start;
292    let mut place_run = |current: &mut usize, run: Vec<PhysicalLayoutObject<A>>| {
293        let mut holes: Vec<(usize, usize)> = Vec::new();
294        for object in run {
295            let size = extent(&object);
296            let alignment = object.4;
297            let reach = current.saturating_sub(HOLE_REACH);
298            holes.retain(|&(_, end)| end > reach);
299            let hole = holes
300                .iter()
301                .position(|&(begin, end)| align_up(begin, alignment) + size <= end);
302            let offset = if let Some(index) = hole {
303                let (begin, end) = holes.swap_remove(index);
304                let offset = align_up(begin, alignment);
305                if offset > begin {
306                    holes.push((begin, offset));
307                }
308                if offset + size < end {
309                    holes.push((offset + size, end));
310                }
311                offset
312            } else {
313                let offset = align_up(*current, alignment);
314                if offset > *current {
315                    holes.push((*current, offset));
316                }
317                *current = offset + size;
318                offset
319            };
320            place(&object, offset);
321        }
322    };
323    place_run(&mut current, common);
324    let segments = lanes
325        .into_iter()
326        .filter(|run| !run.is_empty())
327        .chain(isolated.into_iter().map(|object| vec![object]))
328        .collect::<Vec<_>>();
329    if segments.is_empty() {
330        return current;
331    }
332    for run in segments {
333        current = align_up(current + LANE_SEGMENT_GUARD, LANE_SEGMENT_ALIGN);
334        place_run(&mut current, run);
335    }
336    align_up(current + LANE_SEGMENT_GUARD, LANE_SEGMENT_ALIGN)
337}
338
339impl<A> MemoryLayout<A>
340where
341    A: Copy + Eq + Hash + Ord,
342{
343    pub fn build<S>(source: &S, four_state: bool, mode: MemoryLayoutMode) -> Self
344    where
345        S: LayoutSource<A>,
346    {
347        let input = source.layout_input(mode);
348        let LayoutInput {
349            state_objects,
350            working_addresses,
351            sparse_addresses,
352            unpacked_arrays,
353            requirements,
354            ff_referenced_addresses,
355            num_events,
356            runtime_event_sites,
357            lane_writers,
358            access_rank,
359        } = input;
360
361        let mut stable_objects = state_objects
362            .into_iter()
363            .map(|object| {
364                let size = unpacked_arrays
365                    .get(&object.address)
366                    .map(|layout| layout.plane_size)
367                    .unwrap_or_else(|| get_byte_size(object.width));
368                let alignment = unpacked_arrays
369                    .get(&object.address)
370                    .map(|layout| layout.element_stride.min(8))
371                    .unwrap_or_else(|| get_alignment(object.width));
372                (
373                    object.address,
374                    object.width,
375                    object.is_4state,
376                    size,
377                    alignment,
378                )
379            })
380            .collect::<Vec<_>>();
381        sort_layout_objects(&mut stable_objects, &access_rank);
382
383        let mut offsets = HashMap::default();
384        let mut widths = HashMap::default();
385        let mut is_4states = HashMap::default();
386        let runtime_event_site_layouts = build_runtime_event_site_layouts(&runtime_event_sites);
387        let runtime_event_slot_size = RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
388            + runtime_event_site_layouts
389                .iter()
390                .map(|site| site.payload_words)
391                .max()
392                .unwrap_or(0)
393                * 8;
394
395        let plane_count = if four_state { 2 } else { 1 };
396        let current_offset = place_region(
397            stable_objects,
398            &lane_writers.stable,
399            STATE_HEADER_SIZE,
400            |object| object.3 * plane_count,
401            |&(address, width, is_4state, _, _), offset| {
402                offsets.insert(address, offset);
403                widths.insert(address, width);
404                is_4states.insert(address, is_4state);
405            },
406        );
407
408        let mut working_objects = working_addresses
409            .iter()
410            .map(|address| {
411                let width = widths[address];
412                let size = unpacked_arrays
413                    .get(address)
414                    .map(|layout| layout.plane_size)
415                    .unwrap_or_else(|| get_byte_size(width));
416                let alignment = unpacked_arrays
417                    .get(address)
418                    .map(|layout| layout.element_stride.min(8))
419                    .unwrap_or_else(|| get_alignment(width));
420                (*address, width, is_4states[address], size, alignment)
421            })
422            .collect::<Vec<_>>();
423        sort_layout_objects(&mut working_objects, &access_rank);
424
425        let mut working_offsets = HashMap::default();
426        // Working offsets are relative to a base aligned below; a segment
427        // boundary relative to that base keeps lane segments line-aligned.
428        let working_size = place_region(
429            working_objects,
430            &lane_writers.working,
431            0,
432            |object| object.3 * plane_count,
433            |&(address, ..), offset| {
434                working_offsets.insert(address, offset);
435            },
436        );
437
438        let mut sparse_objects = sparse_addresses
439            .iter()
440            .map(|address| {
441                let width = widths[address];
442                let size = unpacked_arrays
443                    .get(address)
444                    .map(|layout| layout.plane_size)
445                    .unwrap_or_else(|| get_byte_size(width));
446                let alignment = unpacked_arrays
447                    .get(address)
448                    .map(|layout| layout.element_stride.min(8))
449                    .unwrap_or_else(|| get_alignment(width));
450                (*address, width, is_4states[address], size, alignment)
451            })
452            .collect::<Vec<_>>();
453        sort_layout_objects(&mut sparse_objects, &access_rank);
454
455        let mut sparse_offsets = HashMap::default();
456        let sparse_size = place_region(
457            sparse_objects,
458            &lane_writers.sparse,
459            0,
460            |&(_, _, _, size, _)| {
461                let final_chunk_size = align_up(size, 8);
462                let physical_extent = (plane_count - 1) * size + final_chunk_size;
463                align_up(physical_extent, 8)
464            },
465            |&(address, ..), offset| {
466                sparse_offsets.insert(address, offset);
467            },
468        );
469
470        // Region bases keep segment alignment when lanes partition them.
471        let region_alignment = if lane_writers.is_empty() {
472            8
473        } else {
474            LANE_SEGMENT_ALIGN
475        };
476        let working_base_offset = align_up(current_offset, region_alignment);
477        let sparse_base_offset = align_up(working_base_offset + working_size, region_alignment);
478        let mut sparse_metadata_offset = align_up(sparse_base_offset + sparse_size, 8);
479        let mut sparse_layouts = HashMap::default();
480        let mut sparse_order = sparse_addresses;
481        // Group per-object dirty/summary words by writer lane so one lane's
482        // metadata words do not share lines with another lane's.
483        let sparse_lane = |address: &A| {
484            lane_writers
485                .sparse
486                .get(address)
487                .copied()
488                .filter(|mask| mask.is_power_of_two())
489                .map_or(u32::MAX, u64::trailing_zeros)
490        };
491        sparse_order.sort_unstable_by_key(|address| (sparse_lane(address), *address));
492        let mut previous_sparse_lane = None;
493        let sparse_active_capacity = sparse_order.len();
494        for (active_index, address) in sparse_order.into_iter().enumerate() {
495            let lane = sparse_lane(&address);
496            if !lane_writers.is_empty() && previous_sparse_lane != Some(lane) {
497                sparse_metadata_offset = align_up(sparse_metadata_offset, LANE_SEGMENT_ALIGN);
498                previous_sparse_lane = Some(lane);
499            }
500            let chunk_count = unpacked_arrays
501                .get(&address)
502                .map(|layout| layout.plane_size.div_ceil(8))
503                .unwrap_or_else(|| widths[&address].div_ceil(64));
504            let dirty_word_count = chunk_count.div_ceil(64);
505            let summary_word_count = dirty_word_count.div_ceil(64);
506            let dirty_words_offset = sparse_metadata_offset;
507            sparse_metadata_offset += dirty_word_count * 8;
508            let summary_words_offset = sparse_metadata_offset;
509            sparse_metadata_offset += summary_word_count * 8;
510            sparse_layouts.insert(
511                address,
512                SparseWorkingLayout {
513                    active_index,
514                    chunk_count,
515                    dirty_words_offset,
516                    dirty_word_count,
517                    summary_words_offset,
518                    summary_word_count,
519                },
520            );
521        }
522
523        let sparse_active_bits_offset = align_up(sparse_metadata_offset, 8);
524        sparse_metadata_offset =
525            sparse_active_bits_offset + sparse_active_capacity.div_ceil(64) * 8;
526        let triggered_bits_offset = align_up(sparse_metadata_offset, 8);
527        let triggered_bits_total_size = num_events.div_ceil(8);
528        let scratch_base_offset = align_up(triggered_bits_offset + triggered_bits_total_size, 8);
529        let runtime_event_buffer_size =
530            RUNTIME_EVENT_HEADER_SIZE + RUNTIME_EVENT_CAPACITY * runtime_event_slot_size;
531        let merged_total_size = scratch_base_offset;
532
533        let mut address_aliases = requirements.state_aliases.into_iter().collect::<Vec<_>>();
534        address_aliases.sort_unstable();
535        for (alias, canonical) in address_aliases {
536            let fourstate_ok = !four_state
537                || (is_4states.get(&alias) == Some(&false)
538                    && is_4states.get(&canonical) == Some(&false));
539            let alias_fits = widths
540                .get(&alias)
541                .zip(widths.get(&canonical))
542                .is_some_and(|(&alias_width, &canonical_width)| alias_width <= canonical_width);
543            if fourstate_ok
544                && alias_fits
545                && !ff_referenced_addresses.contains(&alias)
546                && let Some(&canonical_offset) = offsets.get(&canonical)
547            {
548                offsets.insert(alias, canonical_offset);
549            }
550        }
551
552        Self {
553            trace: None,
554            four_state,
555            mode,
556            offsets,
557            widths,
558            is_4states,
559            unpacked_arrays,
560            total_size: current_offset,
561            working_offsets,
562            working_base_offset,
563            sparse_offsets,
564            sparse_base_offset,
565            sparse_layouts,
566            sparse_active_bits_offset,
567            sparse_active_capacity,
568            merged_total_size,
569            triggered_bits_offset,
570            triggered_bits_total_size,
571            scratch_base_offset,
572            scratch_size: 0,
573            runtime_event_capacity: RUNTIME_EVENT_CAPACITY,
574            runtime_event_slot_size,
575            runtime_event_buffer_size,
576            runtime_event_site_layouts,
577        }
578    }
579
580    /// Append backend-private scratch storage without changing any semantic
581    /// state offset. Backend planning happens after the backend-neutral state
582    /// layout has been finalized, so scratch is always the final region.
583    pub fn with_backend_scratch(mut self, scratch_size: usize) -> Self {
584        self.scratch_size = scratch_size;
585        self.merged_total_size = align_up(self.scratch_base_offset + scratch_size, 8);
586        self
587    }
588
589    /// Reserve observer activity after scheduler metadata and before backend
590    /// scratch. Stable/working addresses and clock-trigger offsets stay fixed.
591    pub fn enable_trace(&mut self) {
592        if self.trace.is_some() {
593            return;
594        }
595        assert_eq!(
596            self.scratch_size, 0,
597            "trace must be enabled before backend planning"
598        );
599        let homes = self
600            .offsets
601            .iter()
602            .map(|(address, &offset)| {
603                let planes = if self.four_state { 2 } else { 1 };
604                (offset, offset + self.plane_size(address) * planes)
605            })
606            .collect();
607        let trace = TraceLayout::new(self.scratch_base_offset, self.total_size, homes);
608        self.scratch_base_offset = align_up(trace.end_offset(), 8);
609        self.merged_total_size = self.scratch_base_offset;
610        self.trace = Some(trace);
611    }
612
613    pub fn trace_notification_offsets<R: RegionedAddress<A>>(
614        &self,
615        address: &R,
616    ) -> Option<[usize; 2]> {
617        if address.region() != STABLE_REGION {
618            return None;
619        }
620        let trace = self.trace.as_ref()?;
621        Some(trace.notification_offsets(self.offsets[&address.absolute_address()]))
622    }
623
624    pub fn plane_size(&self, address: &A) -> usize {
625        self.unpacked_arrays
626            .get(address)
627            .map(|layout| layout.plane_size)
628            .unwrap_or_else(|| get_byte_size(self.widths[address]))
629    }
630
631    pub fn region_base_offset<R>(&self, address: &R) -> usize
632    where
633        R: RegionedAddress<A>,
634    {
635        let absolute = address.absolute_address();
636        match address.region() {
637            STABLE_REGION => self.offsets[&absolute],
638            SPARSE_WORKING_REGION => self.sparse_base_offset + self.sparse_offsets[&absolute],
639            _ => self.working_base_offset + self.working_offsets[&absolute],
640        }
641    }
642
643    pub fn map_static_bit_offset(&self, address: &A, bit_offset: usize) -> (usize, usize) {
644        let Some(array) = self.unpacked_arrays.get(address) else {
645            return (bit_offset / 8, bit_offset % 8);
646        };
647        let element = bit_offset / array.element_width;
648        let intra_element = bit_offset % array.element_width;
649        (
650            element * array.element_stride + intra_element / 8,
651            intra_element % 8,
652        )
653    }
654
655    pub fn regioned_static_byte_and_intra<R>(
656        &self,
657        address: &R,
658        bit_offset: usize,
659    ) -> Option<(i32, usize)>
660    where
661        R: RegionedAddress<A>,
662    {
663        let absolute = address.absolute_address();
664        let base = match address.region() {
665            STABLE_REGION => *self.offsets.get(&absolute).unwrap_or(&0),
666            SPARSE_WORKING_REGION => {
667                self.sparse_base_offset + *self.sparse_offsets.get(&absolute).unwrap_or(&0)
668            }
669            _ => self.working_base_offset + *self.working_offsets.get(&absolute).unwrap_or(&0),
670        };
671        let (byte, intra) = self.map_static_bit_offset(&absolute, bit_offset);
672        Some((i32::try_from(base.checked_add(byte)?).ok()?, intra))
673    }
674}
675
676pub trait RegionedAddress<A> {
677    fn region(&self) -> u32;
678    fn absolute_address(&self) -> A;
679}
680
681impl<V: Copy> RegionedAddress<AbsoluteAddrBase<V>> for RegionedAbsoluteAddrBase<V> {
682    fn region(&self) -> u32 {
683        self.region
684    }
685
686    fn absolute_address(&self) -> AbsoluteAddrBase<V> {
687        self.absolute_addr()
688    }
689}
690
691fn build_runtime_event_site_layouts(sites: &[RuntimeEventSite]) -> Vec<RuntimeEventSiteLayout> {
692    sites
693        .iter()
694        .map(|site| {
695            let mut payload_words = 0;
696            let args = site
697                .arg_widths
698                .iter()
699                .map(|width| {
700                    let word_count = (*width).div_ceil(64).max(1);
701                    let value_word_offset = payload_words;
702                    payload_words += word_count;
703                    let mask_word_offset = payload_words;
704                    payload_words += word_count;
705                    RuntimeEventArgLayout {
706                        value_word_offset,
707                        mask_word_offset,
708                        word_count,
709                    }
710                })
711                .collect();
712            RuntimeEventSiteLayout {
713                args,
714                payload_words,
715            }
716        })
717        .collect()
718}
719
720const fn align_up(offset: usize, alignment: usize) -> usize {
721    (offset + alignment - 1) & !(alignment - 1)
722}
723
724fn get_alignment(width: usize) -> usize {
725    let size = get_byte_size(width);
726    if size == 0 {
727        1
728    } else if size <= 8 {
729        size.next_power_of_two()
730    } else {
731        8
732    }
733}
734
735pub const fn get_byte_size(width: usize) -> usize {
736    width.div_ceil(8)
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742
743    #[test]
744    fn byte_size_rounds_up_partial_bytes() {
745        assert_eq!(get_byte_size(0), 0);
746        assert_eq!(get_byte_size(1), 1);
747        assert_eq!(get_byte_size(8), 1);
748        assert_eq!(get_byte_size(9), 2);
749    }
750
751    #[test]
752    fn layout_order_is_independent_of_input_iteration_order() {
753        let high_address = (3u32, 64, false, 8, 8);
754        let low_address = (1u32, 64, false, 8, 8);
755        let less_aligned = (0u32, 32, false, 4, 4);
756        let mut forward = vec![high_address, less_aligned, low_address];
757        let mut reverse = forward.iter().copied().rev().collect::<Vec<_>>();
758
759        sort_layout_objects(&mut forward, &HashMap::default());
760        sort_layout_objects(&mut reverse, &HashMap::default());
761
762        assert_eq!(forward, reverse);
763        assert_eq!(forward, vec![low_address, high_address, less_aligned]);
764    }
765
766    #[test]
767    fn layout_requirements_own_semantic_aliases_until_layout() {
768        let mut requirements = LayoutRequirements::default();
769        requirements.state_aliases_mut().insert(2u32, 1u32);
770
771        assert_eq!(requirements.state_aliases().get(&2), Some(&1));
772        assert!(!requirements.is_empty());
773
774        requirements.clear();
775        assert!(requirements.is_empty());
776    }
777
778    #[test]
779    fn layout_applies_aliases_from_requirements() {
780        struct AliasLayoutSource;
781
782        impl LayoutSource<u32> for AliasLayoutSource {
783            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
784                let mut requirements = LayoutRequirements::default();
785                requirements.state_aliases_mut().insert(2, 1);
786                LayoutInput {
787                    state_objects: vec![
788                        StateObjectLayout {
789                            address: 1,
790                            width: 8,
791                            is_4state: false,
792                        },
793                        StateObjectLayout {
794                            address: 2,
795                            width: 8,
796                            is_4state: false,
797                        },
798                    ],
799                    working_addresses: Vec::new(),
800                    sparse_addresses: Vec::new(),
801                    unpacked_arrays: HashMap::default(),
802                    requirements,
803                    ff_referenced_addresses: HashSet::default(),
804                    num_events: 0,
805                    runtime_event_sites: Vec::new(),
806                    lane_writers: Default::default(),
807                    access_rank: Default::default(),
808                }
809            }
810        }
811
812        let layout = MemoryLayout::build(&AliasLayoutSource, false, MemoryLayoutMode::Packed);
813        assert_eq!(layout.offsets[&1], layout.offsets[&2]);
814    }
815
816    #[test]
817    fn access_rank_orders_homes_and_fills_padding_nearby() {
818        struct RankedLayoutSource;
819
820        impl LayoutSource<u32> for RankedLayoutSource {
821            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
822                let object = |address, width| StateObjectLayout {
823                    address,
824                    width,
825                    is_4state: false,
826                };
827                // Access order: 4 (1 byte), 2 (8 bytes), 1 (1 byte), 5 (large),
828                // 3 (8 bytes). 6 is never accessed.
829                let access_rank = [(4, 0), (2, 1), (1, 2), (5, 3), (3, 4)]
830                    .into_iter()
831                    .collect();
832                LayoutInput {
833                    state_objects: vec![
834                        object(1, 8),
835                        object(2, 64),
836                        object(3, 64),
837                        object(4, 8),
838                        object(5, 8 * (LARGE_OBJECT_BYTES + 8)),
839                        object(6, 64),
840                    ],
841                    working_addresses: Vec::new(),
842                    sparse_addresses: Vec::new(),
843                    unpacked_arrays: HashMap::default(),
844                    requirements: LayoutRequirements::default(),
845                    ff_referenced_addresses: HashSet::default(),
846                    num_events: 0,
847                    runtime_event_sites: Vec::new(),
848                    lane_writers: Default::default(),
849                    access_rank,
850                }
851            }
852        }
853
854        let layout = MemoryLayout::build(&RankedLayoutSource, false, MemoryLayoutMode::Packed);
855        let offset = |address: u32| layout.offsets[&address];
856        // Small homes follow access order; the byte accessed after the
857        // eight-byte home fills the padding in front of it.
858        assert_eq!(offset(4), STATE_HEADER_SIZE);
859        assert_eq!(offset(2), STATE_HEADER_SIZE + 8);
860        assert_eq!(offset(1), STATE_HEADER_SIZE + 1);
861        assert_eq!(offset(3), STATE_HEADER_SIZE + 16);
862        // The large home follows every small one, and the unaccessed home
863        // comes last.
864        assert!(offset(5) > offset(3));
865        assert!(offset(6) > offset(5));
866    }
867
868    #[test]
869    fn lane_writers_separate_homes_into_guarded_segments() {
870        struct LaneLayoutSource;
871
872        impl LayoutSource<u32> for LaneLayoutSource {
873            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
874                let mut lane_writers = LaneWriters::default();
875                // 1: lane 0, 2: lane 1, 3: lanes 0 and 1, 4: no parallel writer.
876                lane_writers.stable.insert(1, 0b01);
877                lane_writers.stable.insert(2, 0b10);
878                lane_writers.stable.insert(3, 0b11);
879                lane_writers.working.insert(5, 0b10);
880                let object = |address, width| StateObjectLayout {
881                    address,
882                    width,
883                    is_4state: false,
884                };
885                LayoutInput {
886                    state_objects: vec![
887                        object(1, 24),
888                        object(2, 24),
889                        object(3, 8),
890                        object(4, 8),
891                        object(5, 40),
892                    ],
893                    working_addresses: vec![5],
894                    sparse_addresses: Vec::new(),
895                    unpacked_arrays: HashMap::default(),
896                    requirements: LayoutRequirements::default(),
897                    ff_referenced_addresses: HashSet::default(),
898                    num_events: 0,
899                    runtime_event_sites: Vec::new(),
900                    lane_writers,
901                    access_rank: Default::default(),
902                }
903            }
904        }
905
906        let layout = MemoryLayout::build(&LaneLayoutSource, false, MemoryLayoutMode::Packed);
907        let span = |address: u32| {
908            let start = layout.offsets[&address];
909            (start, start + layout.plane_size(&address))
910        };
911        // Unpartitioned objects stay packed after the header.
912        assert!(span(4).0 < span(1).0 && span(5).0 < span(1).0);
913        for address in [1, 2, 3] {
914            assert_eq!(span(address).0 % LANE_SEGMENT_ALIGN, 0);
915        }
916        // Every pair of segments is separated by at least the guard.
917        let mut segments = [span(1), span(2), span(3)];
918        segments.sort_unstable();
919        for pair in segments.windows(2) {
920            assert!(pair[0].1 + LANE_SEGMENT_GUARD <= pair[1].0, "{segments:?}");
921        }
922        // The next region starts after the trailing guard of the last segment.
923        assert!(segments[2].1 + LANE_SEGMENT_GUARD <= layout.working_base_offset);
924        assert_eq!(layout.working_base_offset % LANE_SEGMENT_ALIGN, 0);
925        assert_eq!(layout.working_offsets[&5] % LANE_SEGMENT_ALIGN, 0);
926    }
927
928    #[test]
929    fn layout_without_lane_writers_is_unchanged_by_partitioning_support() {
930        struct PackedSource;
931
932        impl LayoutSource<u32> for PackedSource {
933            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
934                LayoutInput {
935                    state_objects: (0..6)
936                        .map(|address| StateObjectLayout {
937                            address,
938                            width: 8 + 7 * address as usize,
939                            is_4state: false,
940                        })
941                        .collect(),
942                    working_addresses: vec![1, 3],
943                    sparse_addresses: vec![2],
944                    unpacked_arrays: HashMap::default(),
945                    requirements: LayoutRequirements::default(),
946                    ff_referenced_addresses: HashSet::default(),
947                    num_events: 9,
948                    runtime_event_sites: Vec::new(),
949                    lane_writers: LaneWriters::default(),
950                    access_rank: Default::default(),
951                }
952            }
953        }
954
955        let layout = MemoryLayout::build(&PackedSource, false, MemoryLayoutMode::Packed);
956        // Objects remain tightly packed: the stable region ends right after
957        // the last object instead of after a lane guard.
958        let end = layout
959            .offsets
960            .keys()
961            .map(|address| layout.offsets[address] + layout.plane_size(address))
962            .max()
963            .unwrap();
964        assert_eq!(layout.total_size, end);
965        assert_eq!(layout.working_base_offset, align_up(end, 8));
966    }
967
968    #[test]
969    fn backend_scratch_only_extends_the_final_layout_region() {
970        struct EmptyLayoutSource;
971
972        impl LayoutSource<u32> for EmptyLayoutSource {
973            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
974                LayoutInput {
975                    state_objects: Vec::new(),
976                    working_addresses: Vec::new(),
977                    sparse_addresses: Vec::new(),
978                    unpacked_arrays: HashMap::default(),
979                    requirements: LayoutRequirements::default(),
980                    ff_referenced_addresses: HashSet::default(),
981                    num_events: 3,
982                    runtime_event_sites: Vec::new(),
983                    lane_writers: Default::default(),
984                    access_rank: Default::default(),
985                }
986            }
987        }
988
989        let base = MemoryLayout::build(&EmptyLayoutSource, false, MemoryLayoutMode::Packed);
990        let expanded = base.clone().with_backend_scratch(13);
991
992        assert_eq!(base.scratch_size, 0);
993        assert_eq!(expanded.scratch_base_offset, base.scratch_base_offset);
994        assert_eq!(expanded.scratch_size, 13);
995        assert_eq!(expanded.merged_total_size, base.scratch_base_offset + 16);
996        assert_eq!(expanded.offsets, base.offsets);
997        assert_eq!(expanded.working_offsets, base.working_offsets);
998        assert_eq!(expanded.triggered_bits_offset, base.triggered_bits_offset);
999    }
1000
1001    #[test]
1002    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1003    fn state_header_fields_do_not_overlap() {
1004        const {
1005            assert!(
1006                STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET + 8
1007                    <= STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET
1008            );
1009            assert!(
1010                STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET + 8
1011                    <= STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET
1012            );
1013            assert!(
1014                STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET + 8
1015                    <= STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET
1016            );
1017            assert!(
1018                STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET + 8
1019                    <= STATE_HEADER_EXTERN_FUNCTIONS_ADDR_OFFSET
1020            );
1021            assert!(STATE_HEADER_EXTERN_FUNCTIONS_ADDR_OFFSET + 8 <= STATE_HEADER_SIZE);
1022        }
1023    }
1024}