Skip to main content

celox_runtime/
vcd.rs

1use celox_state_layout::get_byte_size;
2use num_bigint::BigUint;
3use std::fs::File;
4use std::io::{BufWriter, Write};
5use std::path::Path;
6
7/// Describes a signal for VCD recording.
8///
9/// Self-contained — does not reference any IR types. Can be cached
10/// alongside a shared backend artifact so that VCD
11/// works even on cache-hit paths.
12#[derive(Clone, Debug)]
13pub struct VcdSignalDesc {
14    /// VCD scope name (e.g. instance path).
15    pub scope: String,
16    /// Signal name within the scope.
17    pub name: String,
18    /// Byte offset in JIT memory (stable region).
19    pub offset: usize,
20    /// Bit width.
21    pub width: usize,
22    /// Whether this signal has a 4-state mask region immediately after the value.
23    pub is_4state: bool,
24}
25
26/// Describes a signal whose value is supplied by an external runtime rather
27/// than stored in Celox's flat simulation memory.
28#[derive(Clone, Debug)]
29pub struct VcdExternalSignalDesc {
30    pub scope: String,
31    pub name: String,
32    pub width: usize,
33}
34
35#[derive(Clone, Copy)]
36enum VcdWriterSource {
37    Memory { offset: usize, is_4state: bool },
38    External { index: usize },
39}
40
41struct VcdWriterSignal {
42    vcd_id: String,
43    scope: String,
44    name: String,
45    width: usize,
46    source: VcdWriterSource,
47}
48
49pub struct VcdWriter {
50    writer: BufWriter<File>,
51    signals: Vec<VcdWriterSignal>,
52    last_values: Vec<Option<(BigUint, BigUint)>>,
53    timestamp: u64,
54    header_written: bool,
55    external_count: usize,
56}
57
58impl VcdWriter {
59    pub fn new<P: AsRef<Path>>(path: P, descs: &[VcdSignalDesc]) -> std::io::Result<Self> {
60        let file = File::create(path)?;
61        let writer = BufWriter::new(file);
62        let signals = descs
63            .iter()
64            .map(|desc| VcdWriterSignal {
65                vcd_id: String::new(),
66                scope: desc.scope.clone(),
67                name: desc.name.clone(),
68                width: desc.width,
69                source: VcdWriterSource::Memory {
70                    offset: desc.offset,
71                    is_4state: desc.is_4state,
72                },
73            })
74            .collect::<Vec<_>>();
75
76        let last_values = vec![None; signals.len()];
77
78        Ok(Self {
79            writer,
80            signals,
81            last_values,
82            timestamp: 0,
83            header_written: false,
84            external_count: 0,
85        })
86    }
87
88    /// Adds externally supplied signals before the first dump. VCD headers
89    /// cannot be extended after value changes have started.
90    pub fn add_external_signals(&mut self, descs: &[VcdExternalSignalDesc]) -> std::io::Result<()> {
91        if descs.is_empty() {
92            return Ok(());
93        }
94        if self.external_count != 0 {
95            let existing = self
96                .signals
97                .iter()
98                .filter(|signal| matches!(signal.source, VcdWriterSource::External { .. }))
99                .zip(descs)
100                .all(|(signal, desc)| {
101                    signal.scope == desc.scope
102                        && signal.name == desc.name
103                        && signal.width == desc.width
104                });
105            if existing && self.external_count == descs.len() {
106                return Ok(());
107            }
108        }
109        if self.header_written {
110            return Err(std::io::Error::new(
111                std::io::ErrorKind::InvalidInput,
112                "cannot add external VCD signals after the first dump",
113            ));
114        }
115        for desc in descs {
116            let index = self.external_count;
117            self.external_count += 1;
118            self.signals.push(VcdWriterSignal {
119                vcd_id: String::new(),
120                scope: desc.scope.clone(),
121                name: desc.name.clone(),
122                width: desc.width,
123                source: VcdWriterSource::External { index },
124            });
125            self.last_values.push(None);
126        }
127        Ok(())
128    }
129
130    fn write_header(&mut self) -> std::io::Result<()> {
131        if self.header_written {
132            return Ok(());
133        }
134        writeln!(self.writer, "$date")?;
135        writeln!(
136            self.writer,
137            "  {}",
138            chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
139        )?;
140        writeln!(self.writer, "$end")?;
141        writeln!(self.writer, "$version")?;
142        writeln!(self.writer, "  celox")?;
143        writeln!(self.writer, "$end")?;
144        writeln!(self.writer, "$timescale 1ns $end")?;
145
146        let mut scope_order = Vec::<String>::new();
147        let mut scope_groups = Vec::<Vec<usize>>::new();
148        let mut scope_idx = fxhash::FxHashMap::<String, usize>::default();
149        for (signal_index, signal) in self.signals.iter().enumerate() {
150            if let Some(index) = scope_idx.get(&signal.scope).copied() {
151                scope_groups[index].push(signal_index);
152            } else {
153                let index = scope_order.len();
154                scope_idx.insert(signal.scope.clone(), index);
155                scope_order.push(signal.scope.clone());
156                scope_groups.push(vec![signal_index]);
157            }
158        }
159        let mut next_id = 0;
160        for (scope, group) in scope_order.iter().zip(scope_groups) {
161            writeln!(self.writer, "$scope module {} $end", scope)?;
162            for signal_index in group {
163                let signal = &mut self.signals[signal_index];
164                signal.vcd_id = Self::generate_vcd_id(next_id);
165                next_id += 1;
166                writeln!(
167                    self.writer,
168                    "$var wire {} {} {} $end",
169                    signal.width, signal.vcd_id, signal.name
170                )?;
171            }
172            writeln!(self.writer, "$upscope $end")?;
173        }
174        writeln!(self.writer, "$enddefinitions $end")?;
175        writeln!(self.writer, "$dumpvars")?;
176        writeln!(self.writer, "$end")?;
177        self.header_written = true;
178        Ok(())
179    }
180
181    fn generate_vcd_id(num: usize) -> String {
182        let mut id = String::new();
183        let mut n = num;
184        loop {
185            let char = ((n % 94) + 33) as u8 as char;
186            id.push(char);
187            if n < 94 {
188                break;
189            }
190            n = (n / 94) - 1;
191        }
192        id.chars().rev().collect()
193    }
194
195    /// Read a value from the JIT memory at the given offset and width.
196    fn read_value(memory: &[u8], offset: usize, width: usize) -> BigUint {
197        let byte_size = get_byte_size(width);
198        let slice = &memory[offset..offset + byte_size];
199        let mut val = BigUint::from_bytes_le(slice);
200        let extra_bits = byte_size * 8 - width;
201        if extra_bits > 0 {
202            let mask = (BigUint::from(1u32) << width) - 1u32;
203            val &= mask;
204        }
205        val
206    }
207
208    fn mask_to_width(mut value: BigUint, width: usize) -> BigUint {
209        if value.bits() > width as u64 {
210            value &= (BigUint::from(1u8) << width) - 1u8;
211        }
212        value
213    }
214
215    /// Dump all changed signals at the given timestamp.
216    ///
217    /// `memory` is the raw JIT memory (stable region or full buffer).
218    pub fn dump(&mut self, timestamp: u64, memory: &[u8]) -> std::io::Result<()> {
219        self.dump_with_external(timestamp, memory, &[])
220    }
221
222    /// Dump memory-backed signals and external values in registration order.
223    pub fn dump_with_external(
224        &mut self,
225        timestamp: u64,
226        memory: &[u8],
227        external: &[(BigUint, BigUint)],
228    ) -> std::io::Result<()> {
229        if external.len() != self.external_count {
230            return Err(std::io::Error::new(
231                std::io::ErrorKind::InvalidInput,
232                format!(
233                    "expected {} external VCD values, got {}",
234                    self.external_count,
235                    external.len()
236                ),
237            ));
238        }
239        self.write_header()?;
240        if timestamp > self.timestamp || timestamp == 0 {
241            writeln!(self.writer, "#{}", timestamp)?;
242            self.timestamp = timestamp;
243        }
244
245        for (i, sig) in self.signals.iter().enumerate() {
246            let (current_val, current_mask, is_4state) = match sig.source {
247                VcdWriterSource::Memory { offset, is_4state } => {
248                    let byte_size = get_byte_size(sig.width);
249                    let value = Self::read_value(memory, offset, sig.width);
250                    let mask = if is_4state {
251                        Self::read_value(memory, offset + byte_size, sig.width)
252                    } else {
253                        BigUint::from(0u32)
254                    };
255                    (value, mask, is_4state)
256                }
257                VcdWriterSource::External { index } => {
258                    let (value, mask) = &external[index];
259                    let value = Self::mask_to_width(value.clone(), sig.width);
260                    let mask = Self::mask_to_width(mask.clone(), sig.width);
261                    let is_4state = mask != BigUint::default();
262                    (value, mask, is_4state)
263                }
264            };
265
266            let prev = &self.last_values[i];
267            let changed = match prev {
268                Some((pv, pm)) => pv != &current_val || pm != &current_mask,
269                None => true,
270            };
271
272            if changed {
273                if is_4state && current_mask != BigUint::from(0u32) {
274                    Self::write_four_state_value(
275                        &mut self.writer,
276                        sig.width,
277                        &current_val,
278                        &current_mask,
279                        &sig.vcd_id,
280                    )?;
281                } else if sig.width == 1 {
282                    writeln!(self.writer, "{}{}", current_val, sig.vcd_id)?;
283                } else {
284                    writeln!(
285                        self.writer,
286                        "b{} {}",
287                        current_val.to_str_radix(2),
288                        sig.vcd_id
289                    )?;
290                }
291                self.last_values[i] = Some((current_val, current_mask));
292            }
293        }
294        self.writer.flush()?;
295        Ok(())
296    }
297
298    fn write_four_state_value(
299        writer: &mut BufWriter<File>,
300        width: usize,
301        value: &BigUint,
302        mask: &BigUint,
303        vcd_id: &str,
304    ) -> std::io::Result<()> {
305        if width == 1 {
306            let m = mask.bit(0);
307            let v = value.bit(0);
308            let ch = match (m, v) {
309                (false, false) => '0',
310                (false, true) => '1',
311                (true, false) => 'z',
312                (true, true) => 'x',
313            };
314            writeln!(writer, "{}{}", ch, vcd_id)
315        } else {
316            write!(writer, "b")?;
317            for i in (0..width).rev() {
318                let m = mask.bit(i as u64);
319                let v = value.bit(i as u64);
320                let ch = match (m, v) {
321                    (false, false) => '0',
322                    (false, true) => '1',
323                    (true, false) => 'z',
324                    (true, true) => 'x',
325                };
326                write!(writer, "{}", ch)?;
327            }
328            writeln!(writer, " {}", vcd_id)
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn external_values_are_masked_to_their_declared_width() {
339        let dir = tempfile::tempdir().unwrap();
340        let path = dir.path().join("external-width.vcd");
341        let mut writer = VcdWriter::new(&path, &[]).unwrap();
342        writer
343            .add_external_signals(&[VcdExternalSignalDesc {
344                scope: "component".into(),
345                name: "state".into(),
346                width: 8,
347            }])
348            .unwrap();
349
350        writer
351            .dump_with_external(
352                0,
353                &[],
354                &[(BigUint::from(0x1ffu16), BigUint::from(0x100u16))],
355            )
356            .unwrap();
357        writer
358            .dump_with_external(1, &[], &[(BigUint::from(0xffu8), BigUint::default())])
359            .unwrap();
360
361        let dump = std::fs::read_to_string(path).unwrap();
362        assert!(!dump.contains("b111111111"), "{dump}");
363        assert_eq!(dump.matches("b11111111 !").count(), 1, "{dump}");
364    }
365}