Skip to main content

celox_runtime/
simulation.rs

1use bit_set::BitSet;
2use celox_design::DomainKind;
3use fxhash::FxHashMap;
4
5use crate::{
6    AbsoluteAddr, SignalRef, SimulatorErrorCode,
7    backend::{EventHandle, SimBackend},
8    scheduler::{ClockDef, Scheduler, SimEvent},
9};
10
11/// Backend execution hooks needed by the timed simulation engine.
12///
13/// The facade implements this contract to retain policy such as runtime-event
14/// decoration and waveform capture outside the backend-independent scheduler.
15pub trait SimulationExecutor {
16    type Backend: SimBackend;
17
18    fn backend(&self) -> &Self::Backend;
19    fn backend_mut(&mut self) -> &mut Self::Backend;
20    fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode>;
21    fn eval_apply_ff_at(
22        &mut self,
23        event: <Self::Backend as SimBackend>::Event,
24    ) -> Result<(), SimulatorErrorCode>;
25    fn eval_only_ff_at(
26        &mut self,
27        event: <Self::Backend as SimBackend>::Event,
28    ) -> Result<(), SimulatorErrorCode>;
29    fn apply_ff_at(
30        &mut self,
31        event: <Self::Backend as SimBackend>::Event,
32    ) -> Result<(), SimulatorErrorCode>;
33
34    /// Snapshot external-component inputs immediately before an event domain
35    /// evaluates its sequential logic.
36    fn stage_external_event(
37        &mut self,
38        _event: <Self::Backend as SimBackend>::Event,
39        _timestamp: u64,
40    ) -> Result<(), SimulatorErrorCode> {
41        Ok(())
42    }
43
44    /// Fire external-component hooks after the event domain commits and
45    /// before the following combinational settle.
46    fn fire_external_event(
47        &mut self,
48        _event: <Self::Backend as SimBackend>::Event,
49        _timestamp: u64,
50    ) -> Result<(), SimulatorErrorCode> {
51        Ok(())
52    }
53
54    /// Called after the state for a simulation timestamp has stabilized.
55    fn finish_timed_step(&mut self, _timestamp: u64) {}
56}
57
58/// Runtime metadata for one event domain.
59pub struct EventInfo<B: SimBackend> {
60    pub canonical_id: usize,
61    pub is_cascaded: bool,
62    pub eval_ff_event: Option<B::Event>,
63    pub eval_only_event: Option<B::Event>,
64    pub apply_event: Option<B::Event>,
65}
66
67impl<B: SimBackend> Clone for EventInfo<B> {
68    fn clone(&self) -> Self {
69        *self
70    }
71}
72
73impl<B: SimBackend> Copy for EventInfo<B> {}
74
75impl<B: SimBackend> std::fmt::Debug for EventInfo<B> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("EventInfo")
78            .field("canonical_id", &self.canonical_id)
79            .field("is_cascaded", &self.is_cascaded)
80            .field("eval_ff_event", &self.eval_ff_event)
81            .field("eval_only_event", &self.eval_only_event)
82            .field("apply_event", &self.apply_event)
83            .finish()
84    }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88struct PeriodicEventKey {
89    time: u64,
90    event_id: usize,
91    event_addr: AbsoluteAddr,
92    signal: SignalRef,
93    next_val: u8,
94}
95
96impl PeriodicEventKey {
97    fn from_event<B: SimBackend>(event: &SimEvent<B>) -> Self {
98        Self {
99            time: event.time,
100            event_id: event.event_ref.id(),
101            event_addr: event.event_ref.addr(),
102            signal: event.signal,
103            next_val: event.next_val,
104        }
105    }
106}
107
108/// Backend-independent state and execution rules for timed simulation.
109pub struct SimulationState<B: SimBackend> {
110    scheduler: Scheduler<B>,
111    periodic_events: FxHashMap<PeriodicEventKey, usize>,
112    last_clock_values: BitSet,
113    topo_signals: Vec<(SignalRef, usize, usize)>,
114    domain_kinds: Vec<Option<DomainKind>>,
115    event_info: Vec<EventInfo<B>>,
116    signal_to_id: FxHashMap<SignalRef, usize>,
117}
118
119impl<B: SimBackend> SimulationState<B> {
120    /// Rebase edge detection after state was advanced outside this scheduler.
121    pub fn synchronize_event_values(&mut self, backend: &B) {
122        self.last_clock_values.make_empty();
123        for (signal, id, _) in &self.topo_signals {
124            if *id == usize::MAX {
125                continue;
126            }
127            let value: u8 = backend.get_as(*signal);
128            if value != 0 {
129                self.last_clock_values.insert(*id);
130            }
131        }
132    }
133
134    fn replace_triggers_with_stable_edges(&self, backend: &mut B) {
135        backend.clear_triggered_bits();
136        for (signal, id, _) in &self.topo_signals {
137            if *id == usize::MAX {
138                continue;
139            }
140            let was_nonzero = self.last_clock_values.contains(*id);
141            let value: u8 = backend.get_as(*signal);
142            let is_nonzero = value != 0;
143            let triggered = match self.domain_kinds[*id] {
144                Some(DomainKind::ClockPosedge | DomainKind::ResetAsyncHigh) => {
145                    !was_nonzero && is_nonzero
146                }
147                Some(DomainKind::ClockNegedge | DomainKind::ResetAsyncLow) => {
148                    was_nonzero && !is_nonzero
149                }
150                _ => was_nonzero != is_nonzero,
151            };
152            if triggered {
153                backend.mark_triggered_bit(*id);
154            }
155        }
156    }
157
158    pub fn new(
159        backend: &B,
160        topo_signals: Vec<(SignalRef, usize, usize)>,
161        domain_kinds: Vec<Option<DomainKind>>,
162        event_info: Vec<EventInfo<B>>,
163    ) -> Self {
164        let mut last_clock_values = BitSet::with_capacity(backend.num_events());
165        let mut signal_to_id = FxHashMap::default();
166        for (signal, id, _) in topo_signals.iter().copied() {
167            if id == usize::MAX {
168                continue;
169            }
170            signal_to_id.insert(signal, id);
171            let value: u8 = backend.get_as(signal);
172            if value != 0 {
173                last_clock_values.insert(id);
174            }
175        }
176
177        Self {
178            scheduler: Scheduler::new(),
179            periodic_events: FxHashMap::default(),
180            last_clock_values,
181            topo_signals,
182            domain_kinds,
183            event_info,
184            signal_to_id,
185        }
186    }
187
188    pub fn add_clock(
189        &mut self,
190        event: B::Event,
191        signal: SignalRef,
192        period: u64,
193        initial_delay: u64,
194    ) {
195        let event_id = event.id();
196        if event_id >= self.scheduler.clocks.len() {
197            self.scheduler.clocks.resize(event_id + 1, None);
198        }
199        self.scheduler.clocks[event_id] = Some(ClockDef { period });
200        self.push_periodic_event(SimEvent {
201            time: initial_delay,
202            event_ref: event,
203            signal,
204            next_val: 1,
205        });
206    }
207
208    pub fn schedule(&mut self, event: B::Event, signal: SignalRef, time: u64, value: u8) {
209        self.scheduler.push(SimEvent {
210            time,
211            event_ref: event,
212            signal,
213            next_val: value,
214        });
215    }
216
217    fn push_periodic_event(&mut self, event: SimEvent<B>) {
218        *self
219            .periodic_events
220            .entry(PeriodicEventKey::from_event(&event))
221            .or_default() += 1;
222        self.scheduler.push(event);
223    }
224
225    pub fn step<E>(&mut self, executor: &mut E) -> Result<Option<u64>, SimulatorErrorCode>
226    where
227        E: SimulationExecutor<Backend = B>,
228    {
229        let (current_time, events_to_process) = match self.scheduler.pop_all_at_next_time() {
230            Some(events) => events,
231            None => return Ok(None),
232        };
233        self.step_events(executor, current_time, events_to_process)
234    }
235
236    /// Settle externally driven state against the previous edge baseline even
237    /// when no clock/reset signal is explicitly scheduled at this timestamp.
238    pub fn settle_at<E>(
239        &mut self,
240        executor: &mut E,
241        time: u64,
242    ) -> Result<Option<u64>, SimulatorErrorCode>
243    where
244        E: SimulationExecutor<Backend = B>,
245    {
246        self.step_events(executor, time, Vec::new())
247    }
248
249    fn step_events<E>(
250        &mut self,
251        executor: &mut E,
252        current_time: u64,
253        events_to_process: Vec<SimEvent<B>>,
254    ) -> Result<Option<u64>, SimulatorErrorCode>
255    where
256        E: SimulationExecutor<Backend = B>,
257    {
258        self.scheduler.time = current_time;
259
260        // Keep periodic provenance private so the public SimEvent shape stays
261        // stable. Consume matching sidecar counts before fallible work so an
262        // execution error cannot leave a stale periodic marker behind.
263        let mut periodic_events_to_process: Vec<SimEvent<B>> = Vec::new();
264        for event in &events_to_process {
265            let key = PeriodicEventKey::from_event(event);
266            let mut remove_key = false;
267            if let Some(count) = self.periodic_events.get_mut(&key) {
268                periodic_events_to_process.push(SimEvent {
269                    time: event.time,
270                    event_ref: event.event_ref,
271                    signal: event.signal,
272                    next_val: event.next_val,
273                });
274                *count -= 1;
275                remove_key = *count == 0;
276            }
277            if remove_key {
278                self.periodic_events.remove(&key);
279            }
280        }
281        debug_assert!(
282            self.periodic_events
283                .keys()
284                .all(|event| event.time > current_time),
285            "periodic event sidecar fell behind the scheduler"
286        );
287
288        let num_events = executor.backend().num_events();
289        for event in &events_to_process {
290            executor.backend_mut().set(event.signal, event.next_val);
291        }
292
293        let mut triggered_domains = BitSet::with_capacity(num_events);
294        let mut discovered_in_this_step = BitSet::with_capacity(num_events);
295        let mut scheduled_trigger_ids = BitSet::with_capacity(num_events);
296        // An external drive may already have been combinationally evaluated
297        // by a testbench read. Compare against our saved baseline even if that
298        // evaluation's transient trigger bits have since been cleared.
299        let mut track_stable_edges = events_to_process.is_empty();
300        executor.backend_mut().clear_triggered_bits();
301
302        for event in &events_to_process {
303            if let Some(&id) = self.signal_to_id.get(&event.signal) {
304                track_stable_edges = true;
305                let was_nonzero = self.last_clock_values.contains(id);
306                let is_nonzero = event.next_val != 0;
307                let triggered = match self.domain_kinds[id] {
308                    Some(DomainKind::ClockPosedge | DomainKind::ResetAsyncHigh) => {
309                        !was_nonzero && is_nonzero
310                    }
311                    Some(DomainKind::ClockNegedge | DomainKind::ResetAsyncLow) => {
312                        was_nonzero && !is_nonzero
313                    }
314                    _ => !was_nonzero && is_nonzero,
315                };
316                if triggered {
317                    scheduled_trigger_ids.insert(id);
318                    executor.backend_mut().mark_triggered_bit(id);
319                }
320            }
321        }
322
323        executor.eval_comb()?;
324        if track_stable_edges {
325            // Combinational settling before an active scheduled source domain
326            // commits may expose transient derived-clock edges. In that case,
327            // keep only the source event and rediscover stable edges after the
328            // commit. If the source edge is inactive, preserve derived edges
329            // while filtering out the scheduled signal's own transition.
330            if scheduled_trigger_ids.is_empty() {
331                self.replace_triggers_with_stable_edges(executor.backend_mut());
332            } else {
333                executor.backend_mut().clear_triggered_bits();
334                for id in scheduled_trigger_ids.iter() {
335                    executor.backend_mut().mark_triggered_bit(id);
336                }
337            }
338        }
339
340        let mut comb_already_done = false;
341        loop {
342            let mut any_new_outer_loop_trigger = false;
343            let mut newly_triggered = Vec::new();
344
345            loop {
346                let mut any_new_sequential_trigger = false;
347                let marked_bits = executor.backend().get_triggered_bits();
348                executor.backend_mut().clear_triggered_bits();
349
350                let mut can_use_eval_apply =
351                    triggered_domains.is_empty() && marked_bits.count() == 1;
352                if can_use_eval_apply {
353                    let single_id = marked_bits.iter().next().expect("one marked trigger");
354                    let info = self.event_info[single_id];
355                    can_use_eval_apply = !info.is_cascaded;
356                    if can_use_eval_apply {
357                        if let Some(event) = info.eval_ff_event {
358                            discovered_in_this_step.insert(single_id);
359                            triggered_domains.insert(info.canonical_id);
360                            any_new_outer_loop_trigger = true;
361                            executor.stage_external_event(event, current_time)?;
362                            executor.eval_apply_ff_at(event)?;
363                            executor.fire_external_event(event, current_time)?;
364                            executor.eval_comb()?;
365                            if track_stable_edges {
366                                self.replace_triggers_with_stable_edges(executor.backend_mut());
367                            }
368                            comb_already_done = true;
369                            break;
370                        }
371                    }
372                }
373
374                for id in marked_bits.iter() {
375                    if discovered_in_this_step.contains(id) {
376                        continue;
377                    }
378                    discovered_in_this_step.insert(id);
379
380                    let info = self.event_info[id];
381                    if triggered_domains.contains(info.canonical_id) {
382                        continue;
383                    }
384                    triggered_domains.insert(info.canonical_id);
385                    any_new_sequential_trigger = true;
386                    newly_triggered.push(info.canonical_id);
387
388                    if let Some(event) = info.eval_only_event {
389                        executor.stage_external_event(
390                            info.eval_ff_event.unwrap_or(event),
391                            current_time,
392                        )?;
393                        executor.eval_only_ff_at(event)?;
394                    } else if let Some(event) = info.eval_ff_event {
395                        executor.stage_external_event(event, current_time)?;
396                        executor.eval_apply_ff_at(event)?;
397                    } else {
398                        unreachable!(
399                            "FF trigger discovered without a corresponding execution unit"
400                        );
401                    }
402                }
403
404                if !any_new_sequential_trigger {
405                    break;
406                }
407            }
408
409            if newly_triggered.is_empty() && !any_new_outer_loop_trigger {
410                break;
411            }
412
413            for id in &newly_triggered {
414                if let Some(event) = self.event_info[*id].apply_event {
415                    executor.apply_ff_at(event)?;
416                }
417            }
418            for id in &newly_triggered {
419                if let Some(event) = self.event_info[*id].eval_ff_event {
420                    executor.fire_external_event(event, current_time)?;
421                }
422            }
423
424            if comb_already_done {
425                comb_already_done = false;
426            } else {
427                executor.eval_comb()?;
428                if track_stable_edges {
429                    self.replace_triggers_with_stable_edges(executor.backend_mut());
430                }
431            }
432        }
433
434        for (signal, id, _) in &self.topo_signals {
435            if *id == usize::MAX {
436                continue;
437            }
438            let value: u8 = executor.backend().get_as(*signal);
439            if value != 0 {
440                self.last_clock_values.insert(*id);
441            } else {
442                self.last_clock_values.remove(*id);
443            }
444        }
445
446        for event in periodic_events_to_process {
447            let event_id = event.event_ref.id();
448            if let Some(Some(clock)) = self.scheduler.clocks.get(event_id) {
449                self.push_periodic_event(SimEvent {
450                    time: current_time + clock.period / 2,
451                    event_ref: event.event_ref,
452                    signal: event.signal,
453                    next_val: 1 - event.next_val,
454                });
455            }
456        }
457
458        executor.finish_timed_step(current_time);
459        Ok(Some(current_time))
460    }
461
462    pub fn time(&self) -> u64 {
463        self.scheduler.time
464    }
465
466    pub fn set_time(&mut self, time: u64) {
467        self.scheduler.time = time;
468    }
469
470    pub fn next_event_time(&self) -> Option<u64> {
471        self.scheduler.next_event_time()
472    }
473}