Skip to main content

logicaffeine_compile/codegen_sva/
dram.rs

1//! dram — the DRAM machine.
2//!
3//! The handout claims ">90% bus efficiency, because the compiler schedules row hits and
4//! interleaves banks deterministically" and "refresh is co-scheduled, never a surprise."
5//! This module makes those MEASUREMENTS:
6//!
7//!   • [`DramTimings`] — an LPDDR5X-class timing table (one struct of data, so the
8//!     memory architect's corrections drop in as numbers, not code);
9//!   • [`schedule`] / [`schedule_ordered`] — the compile-time command scheduler: stream descriptors
10//!     (the Wave-1 static schedule's vocabulary) → a fully TIMED command stream with
11//!     bank-rotated row pipelining and per-bank refresh placed into planned slots;
12//!   • [`check_legal`] — an INDEPENDENT per-command legality checker (tRCD/tRP/tRAS/
13//!     tRC/tRRD/tFAW/tCCD, data-bus occupancy, refresh deadlines) — the scheduler is
14//!     never trusted to grade its own homework;
15//!   • [`DeviceModel`] — a bank-state device that EXECUTES a timed stream and reports
16//!     every data-beat time: the zero-jitter fact is `planned == executed`, cycle for
17//!     cycle;
18//!   • blacklist remapping — a compile-time row-remap around known-bad rows: the same
19//!     payload moves, legally, with a measured efficiency delta (yield-by-recompile).
20//!
21//! Everything is a pure function of its inputs — deterministic by construction, like
22//! the rest of the stack. Wave 3 grows the certified-SAT legality obligation and the
23//! in-order executor RTL on top of these exact command streams.
24
25use std::collections::BTreeMap;
26
27/// LPDDR5X-class timing parameters, in CONTROLLER CLOCK CYCLES, plus geometry.
28/// Values are engineering-realistic for an LPDDR5X-6400-class part driven by a
29/// controller clock where one RD/WR burst (BL16) occupies `data_cycles` on the bus —
30/// the table is DATA for the memory architect to correct, and every measurement in
31/// this module re-derives from it.
32#[derive(Clone, Debug)]
33pub struct DramTimings {
34    pub banks: u32,
35    /// Row (page) size in bytes.
36    pub row_bytes: u64,
37    /// Bytes transferred by one RD/WR burst.
38    pub burst_bytes: u64,
39    /// Cycles one burst occupies the data bus.
40    pub data_cycles: u64,
41    /// ACT → RD/WR to the same bank.
42    pub trcd: u64,
43    /// PRE → ACT to the same bank.
44    pub trp: u64,
45    /// ACT → PRE minimum to the same bank.
46    pub tras: u64,
47    /// ACT → ACT to the same bank.
48    pub trc: u64,
49    /// ACT → ACT to DIFFERENT banks.
50    pub trrd: u64,
51    /// Rolling window in which at most four ACTs may issue.
52    pub tfaw: u64,
53    /// CAS → CAS (any bank) — data-bus burst spacing.
54    pub tccd: u64,
55    /// Per-bank refresh (REFpb) duration: REFpb → next ACT to that bank.
56    pub trfc_pb: u64,
57    /// Per-bank refresh interval: every bank needs one REFpb per window this long.
58    pub trefi_pb: u64,
59    /// Write recovery: last WRITE burst end → PRE on that bank.
60    pub twr: u64,
61    /// Data-bus turnaround bubble when switching read↔write direction.
62    pub tturn: u64,
63    /// Above 85 °C the refresh interval halves.
64    pub hot: bool,
65}
66
67impl DramTimings {
68    /// An LPDDR5X-6400-class nominal-corner timing table (16 banks, 2 KB rows, BL16 ≈ 32 B
69    /// bursts). The `t*` counts below are JEDEC LPDDR5X typical timings for that speed grade,
70    /// expressed in the controller's tick unit — a representative nominal corner, NOT a datasheet
71    /// pin for a specific die. Validate them against the exact part's datasheet before production.
72    pub fn lpddr5x() -> Self {
73        DramTimings {
74            banks: 16,
75            row_bytes: 2048,
76            burst_bytes: 32,
77            data_cycles: 8,
78            trcd: 15,
79            trp: 15,
80            tras: 34,
81            trc: 49,
82            trrd: 8,
83            tfaw: 40,
84            tccd: 8,
85            trfc_pb: 112,
86            trefi_pb: 3906,
87            twr: 18,
88            tturn: 6,
89            hot: false,
90        }
91    }
92
93    /// Micron MT47H64M16 (1 Gb, x16, 8 banks) at DDR2-800 (-25E speed grade, tCK = 2.5 ns),
94    /// JEDEC 5-5-5 timings expressed in DDR-clock cycles. This is the on-board DRAM of the
95    /// Nexys A7-100T and the timing FLOOR the ComputeDRAM streams deliberately violate.
96    /// NOTE: DDR2 has all-bank AUTO-REFRESH only (no per-bank REFpb); the RefPb schema here
97    /// models refresh as per-bank for schedule/checker compatibility — flagged, and irrelevant
98    /// to the ACT-PRE-ACT compute path this campaign exercises.
99    pub fn ddr2_micron() -> Self {
100        DramTimings {
101            banks: 8,
102            row_bytes: 2048, // x16 1 Gb page = 2 KB
103            burst_bytes: 16, // BL8 × 2 bytes (x16)
104            data_cycles: 4,  // BL8 = 8 beats = 4 tCK
105            trcd: 5,         // 12.5 ns
106            trp: 5,          // 12.5 ns
107            tras: 18,        // 45 ns
108            trc: 23,         // 57.5 ns (= tRAS + tRP)
109            trrd: 4,         // 10 ns (2 KB page)
110            tfaw: 18,        // 45 ns (2 KB page; ≥ 4 × tRRD)
111            tccd: 2,         // 2 tCK
112            trfc_pb: 51,     // 127.5 ns (1 Gb)
113            trefi_pb: 3120,  // 7.8 µs
114            twr: 6,          // 15 ns
115            tturn: 2,
116            hot: false,
117        }
118    }
119
120    pub fn trefi_effective(&self) -> u64 {
121        if self.hot {
122            self.trefi_pb / 2
123        } else {
124            self.trefi_pb
125        }
126    }
127}
128
129/// A compile-time transfer descriptor — the vocabulary the Wave-1 static schedule
130/// hands to the memory edge: `count` elements of `elem_bytes` from `base`, advancing
131/// `stride_bytes` per element (stride == elem ⇒ sequential; stride > row ⇒ row-per-
132/// element; the dynamic tail is modeled as an explicit gather list, see [`Gather`]).
133#[derive(Clone, Debug)]
134pub struct StreamDesc {
135    pub base: u64,
136    pub elem_bytes: u64,
137    pub stride_bytes: u64,
138    pub count: u64,
139    pub write: bool,
140}
141
142/// The dynamic tail: explicit addresses (a pointer-chase), one element each.
143#[derive(Clone, Debug)]
144pub struct Gather {
145    pub addrs: Vec<u64>,
146    pub elem_bytes: u64,
147    pub write: bool,
148}
149
150/// One DRAM command.
151#[derive(Clone, Copy, PartialEq, Eq, Debug)]
152pub enum DramCmd {
153    Act { bank: u32, row: u64 },
154    Rd { bank: u32 },
155    Wr { bank: u32 },
156    Pre { bank: u32 },
157    RefPb { bank: u32 },
158}
159
160/// A command with its planned issue cycle. The PLAN is the artifact: Wave 3 proves
161/// windows of it timing-legal in the certified SAT, and the executor RTL runs it
162/// in order with no interpretation.
163#[derive(Clone, Copy, Debug)]
164pub struct TimedCmd {
165    pub at: u64,
166    pub cmd: DramCmd,
167}
168
169/// Address → (bank, row): rows rotate across banks so sequential rows pipeline
170/// (bank k streams data while bank k+1 activates). A compile-time-known mapping —
171/// the schedule depends on it, and so does the blacklist remap.
172pub fn map_addr(t: &DramTimings, addr: u64) -> (u32, u64) {
173    let row_index = addr / t.row_bytes;
174    let bank = (row_index % t.banks as u64) as u32;
175    (bank, row_index / t.banks as u64)
176}
177
178/// How multi-stream access lists are ordered into the plan.
179#[derive(Clone, Copy, PartialEq, Eq, Debug)]
180pub enum VisitOrder {
181    /// Streams back-to-back in declaration order (single-stream kernels).
182    ProgramOrder,
183    /// ROW-TILED round-robin: one full row from each stream in rotation — the
184    /// compile-time answer to SoA kernels (nbody reads 7 arrays per iteration; a
185    /// reactive controller thrashes rows, the static scheduler TILES them, because
186    /// it is allowed to reorder anything without a data dependence).
187    RowTiled,
188}
189
190fn one_stream_bursts(t: &DramTimings, s: &StreamDesc) -> Vec<(u32, u64, bool)> {
191    let mut out = Vec::new();
192    let mut last_burst = u64::MAX;
193    for i in 0..s.count {
194        let addr = s.base + i * s.stride_bytes;
195        for b in 0..s.elem_bytes.div_ceil(t.burst_bytes).max(1) {
196            let burst = (addr + b * t.burst_bytes) / t.burst_bytes;
197            if burst != last_burst {
198                last_burst = burst;
199                let (bank, row) = map_addr(t, burst * t.burst_bytes);
200                out.push((bank, row, s.write));
201            }
202        }
203    }
204    out
205}
206
207/// Expand descriptors into the per-burst access list: each entry is (bank, row,
208/// write). Bursts within one row are contiguous `burst_bytes` chunks.
209fn burst_list(
210    t: &DramTimings,
211    streams: &[StreamDesc],
212    gathers: &[Gather],
213    order: VisitOrder,
214) -> Vec<(u32, u64, bool)> {
215    let mut out = Vec::new();
216    match order {
217        VisitOrder::ProgramOrder => {
218            for s in streams {
219                out.extend(one_stream_bursts(t, s));
220            }
221        }
222        VisitOrder::RowTiled => {
223            // Split each stream's bursts into row-runs, then rotate one run at a time.
224            let mut runs: Vec<Vec<Vec<(u32, u64, bool)>>> = streams
225                .iter()
226                .map(|s| {
227                    let bursts = one_stream_bursts(t, s);
228                    let mut rows: Vec<Vec<(u32, u64, bool)>> = Vec::new();
229                    for b in bursts {
230                        match rows.last_mut() {
231                            Some(run) if run[0].0 == b.0 && run[0].1 == b.1 => run.push(b),
232                            _ => rows.push(vec![b]),
233                        }
234                    }
235                    rows
236                })
237                .collect();
238            let mut idx = vec![0usize; runs.len()];
239            loop {
240                let mut any = false;
241                for (s, run_list) in runs.iter_mut().enumerate() {
242                    if idx[s] < run_list.len() {
243                        out.append(&mut run_list[idx[s]]);
244                        idx[s] += 1;
245                        any = true;
246                    }
247                }
248                if !any {
249                    break;
250                }
251            }
252        }
253    }
254    for g in gathers {
255        for &addr in &g.addrs {
256            let (bank, row) = map_addr(t, addr);
257            out.push((bank, row, g.write));
258        }
259    }
260    out
261}
262
263/// The compile-time scheduler: bursts grouped into row visits, rows pipelined across
264/// banks (ACT ahead under tRRD/tFAW while the data bus streams), refresh co-scheduled
265/// per bank into planned slots before every deadline. Returns the timed plan.
266///
267/// `bad_rows` is the yield/blacklist remap input: any (bank,row) in it is skipped and
268/// its data lands on the next clean row of the same bank — a pure compile-time remap
269/// (the program's payload is unchanged; only physical placement moves).
270pub fn schedule(
271    t: &DramTimings,
272    streams: &[StreamDesc],
273    gathers: &[Gather],
274    bad_rows: &[(u32, u64)],
275) -> Vec<TimedCmd> {
276    schedule_ordered(t, streams, gathers, bad_rows, VisitOrder::ProgramOrder)
277}
278
279/// [`schedule`] with an explicit multi-stream visit order (see [`VisitOrder`]).
280pub fn schedule_ordered(
281    t: &DramTimings,
282    streams: &[StreamDesc],
283    gathers: &[Gather],
284    bad_rows: &[(u32, u64)],
285    order: VisitOrder,
286) -> Vec<TimedCmd> {
287    let bursts = burst_list(t, streams, gathers, order);
288
289    // Blacklist remap: per bank, a sorted set of bad rows; row r maps to r + (number
290    // of bad rows ≤ its remapped position) — computed by walking clean rows in order.
291    let mut bad: BTreeMap<u32, Vec<u64>> = BTreeMap::new();
292    for &(b, r) in bad_rows {
293        bad.entry(b).or_default().push(r);
294    }
295    for v in bad.values_mut() {
296        v.sort_unstable();
297        v.dedup();
298    }
299    let remap = |bank: u32, row: u64| -> u64 {
300        match bad.get(&bank) {
301            None => row,
302            Some(list) => {
303                // Skip bad rows: the k-th clean row of the bank.
304                let mut r = row;
305                for &b in list {
306                    if b <= r {
307                        r += 1;
308                    } else {
309                        break;
310                    }
311                }
312                // A run of bad rows directly at/after the shift needs another pass.
313                while list.binary_search(&r).is_ok() {
314                    r += 1;
315                }
316                r
317            }
318        }
319    };
320
321    // Group consecutive same-(bank,row) bursts into row visits.
322    struct Visit {
323        bank: u32,
324        row: u64,
325        bursts: Vec<bool>, // write flags, in order
326    }
327    let mut visits: Vec<Visit> = Vec::new();
328    for (bank, row, write) in bursts {
329        let row = remap(bank, row);
330        match visits.last_mut() {
331            Some(v) if v.bank == bank && v.row == row => v.bursts.push(write),
332            _ => visits.push(Visit {
333                bank,
334                row,
335                bursts: vec![write],
336            }),
337        }
338    }
339
340    // ── The plan is CLOSED-PAGE, bank-rotated, with BACKGROUND refresh ────────────
341    // Closed-page: every row visit is self-contained (ACT … CAS… PRE) — sequential
342    // rotation never revisits a row, so open-page buys nothing, and a closed bank is
343    // always refresh-ready (the "fixed-latency closed-page" of the rung-3 DRAM).
344    // Background refresh: each bank refreshes at STAGGERED ABSOLUTE times planned at
345    // compile time; REFpb on an idle bank costs ZERO data-bus cycles, so the tax only
346    // appears when a refresh collides with that bank's own visit — rare under 16-way
347    // rotation, and even then planned, never a surprise.
348    let nb = t.banks as usize;
349    let trefi = t.trefi_effective();
350    let mut idle_ready: Vec<u64> = vec![0; nb]; // bank fully idle & tRP/tRFC elapsed
351    let mut last_act_bank: Vec<u64> = vec![u64::MAX; nb]; // for tRC (u64::MAX = never)
352    let mut next_ref: Vec<u64> = (0..nb)
353        .map(|i| (i as u64 + 1) * trefi / nb as u64)
354        .collect();
355    let mut acts: Vec<u64> = Vec::new(); // ACT timestamps for tRRD/tFAW
356    let mut bus_free: u64 = 0;
357    let mut last_dir: Option<bool> = None; // false = read, true = write
358    let mut out: Vec<TimedCmd> = Vec::new();
359
360    fn place_act(t: &DramTimings, acts: &mut Vec<u64>, mut at: u64) -> u64 {
361        if let Some(&last) = acts.last() {
362            at = at.max(last + t.trrd);
363        }
364        loop {
365            let win_start = at.saturating_sub(t.tfaw - 1);
366            let in_window = acts.iter().filter(|&&a| a >= win_start).count();
367            if in_window < 4 {
368                break;
369            }
370            let oldest = acts
371                .iter()
372                .copied()
373                .filter(|&a| a >= win_start)
374                .min()
375                .unwrap();
376            at = oldest + t.tfaw;
377        }
378        acts.push(at);
379        let horizon = at.saturating_sub(2 * t.tfaw);
380        acts.retain(|&a| a >= horizon);
381        at
382    }
383
384    let drain_refreshes = |bank: usize,
385                           before: u64,
386                           idle_ready: &mut Vec<u64>,
387                           next_ref: &mut Vec<u64>,
388                           out: &mut Vec<TimedCmd>| {
389        while next_ref[bank] <= before {
390            let r_at = next_ref[bank].max(idle_ready[bank]);
391            out.push(TimedCmd {
392                at: r_at,
393                cmd: DramCmd::RefPb { bank: bank as u32 },
394            });
395            idle_ready[bank] = r_at + t.trfc_pb;
396            next_ref[bank] += trefi;
397        }
398    };
399
400    for v in visits.iter() {
401        let bank = v.bank as usize;
402
403        // Take any refresh slot planned before this visit's data would start.
404        drain_refreshes(bank, bus_free, &mut idle_ready, &mut next_ref, &mut out);
405
406        // ACT: backdated toward (bus_free - tRCD) so the first CAS lands exactly when
407        // the bus frees — the pipeline overlap that makes rotation near-peak.
408        let mut act_lb = idle_ready[bank];
409        if last_act_bank[bank] != u64::MAX {
410            act_lb = act_lb.max(last_act_bank[bank] + t.trc);
411        }
412        let want = bus_free.saturating_sub(t.trcd);
413        let act_at = place_act(t, &mut acts, act_lb.max(want.min(u64::MAX)));
414        out.push(TimedCmd {
415            at: act_at,
416            cmd: DramCmd::Act {
417                bank: v.bank,
418                row: v.row,
419            },
420        });
421        last_act_bank[bank] = act_at;
422
423        // Bursts: CAS spaced by the data bus, with a turnaround bubble on any
424        // read↔write direction switch (planned, like everything else).
425        let mut cas_at = (act_at + t.trcd).max(bus_free);
426        let mut last_cas_end = 0u64;
427        let mut last_write_end = 0u64;
428        for &write in &v.bursts {
429            let turn = if last_dir.is_some_and(|d| d != write) {
430                t.tturn
431            } else {
432                0
433            };
434            cas_at = cas_at.max(bus_free + turn);
435            let cmd = if write {
436                DramCmd::Wr { bank: v.bank }
437            } else {
438                DramCmd::Rd { bank: v.bank }
439            };
440            out.push(TimedCmd { at: cas_at, cmd });
441            bus_free = cas_at + t.tccd.max(t.data_cycles);
442            last_cas_end = cas_at + t.data_cycles;
443            if write {
444                last_write_end = last_cas_end;
445            }
446            last_dir = Some(write);
447            cas_at += t.tccd.max(t.data_cycles);
448        }
449
450        // Close the page (write recovery honored).
451        let pre_at = (act_at + t.tras)
452            .max(last_cas_end)
453            .max(if last_write_end > 0 {
454                last_write_end + t.twr
455            } else {
456                0
457            });
458        out.push(TimedCmd {
459            at: pre_at,
460            cmd: DramCmd::Pre { bank: v.bank },
461        });
462        idle_ready[bank] = pre_at + t.trp;
463    }
464
465    // Background refresh obligations for EVERY bank across the whole plan span.
466    let span = out.iter().map(|c| c.at).max().unwrap_or(0);
467    for bank in 0..nb {
468        drain_refreshes(bank, span, &mut idle_ready, &mut next_ref, &mut out);
469    }
470
471    out.sort_by(|a, b| a.at.cmp(&b.at));
472    out
473}
474
475/// The measured outcome of a plan.
476#[derive(Clone, Copy, Debug, PartialEq)]
477pub struct Efficiency {
478    pub data_bus_cycles: u64,
479    pub span_cycles: u64,
480    pub bursts: u64,
481    pub row_activates: u64,
482    pub refreshes: u64,
483}
484
485impl Efficiency {
486    pub fn value(&self) -> f64 {
487        if self.span_cycles == 0 {
488            1.0
489        } else {
490            self.data_bus_cycles as f64 / self.span_cycles as f64
491        }
492    }
493}
494
495pub fn measure(t: &DramTimings, plan: &[TimedCmd]) -> Efficiency {
496    let mut e = Efficiency {
497        data_bus_cycles: 0,
498        span_cycles: 0,
499        bursts: 0,
500        row_activates: 0,
501        refreshes: 0,
502    };
503    let mut last_end = 0u64;
504    for c in plan {
505        match c.cmd {
506            DramCmd::Rd { .. } | DramCmd::Wr { .. } => {
507                e.bursts += 1;
508                e.data_bus_cycles += t.data_cycles;
509                last_end = last_end.max(c.at + t.data_cycles);
510            }
511            DramCmd::Act { .. } => {
512                e.row_activates += 1;
513                last_end = last_end.max(c.at + 1);
514            }
515            DramCmd::RefPb { .. } => {
516                e.refreshes += 1;
517                last_end = last_end.max(c.at + t.trfc_pb);
518            }
519            DramCmd::Pre { .. } => last_end = last_end.max(c.at + 1),
520        }
521    }
522    e.span_cycles = last_end;
523    e
524}
525
526/// The INDEPENDENT legality checker — a total function over the plan, sharing no code
527/// with the scheduler. Returns the first violation as `Err(description)`.
528pub fn check_legal(t: &DramTimings, plan: &[TimedCmd]) -> Result<(), String> {
529    let nb = t.banks as usize;
530    #[derive(Clone, Copy, PartialEq)]
531    enum BankState {
532        Idle,
533        Active,
534    }
535    let mut state = vec![BankState::Idle; nb];
536    let mut act_at = vec![0u64; nb];
537    /// Earliest next ACT per bank (tRC from its last ACT).
538    let mut act_ready = vec![0u64; nb];
539    /// Earliest the bank counts as idle-and-settled (tRP after PRE, tRFC after REFpb).
540    let mut idle_ready = vec![0u64; nb];
541    let mut last_cas_end = vec![0u64; nb];
542    let mut acts: Vec<u64> = Vec::new();
543    let mut last_act: Option<u64> = None;
544    let mut bus_busy_until = 0u64;
545    let mut bus_last_dir: Option<bool> = None;
546    let mut last_write_end = vec![0u64; nb];
547    let mut refs: Vec<Vec<u64>> = vec![Vec::new(); nb];
548
549    let mut prev_at = 0u64;
550    for (i, c) in plan.iter().enumerate() {
551        if c.at < prev_at {
552            return Err(format!("cmd {i}: plan not time-sorted"));
553        }
554        prev_at = c.at;
555        match c.cmd {
556            DramCmd::Act { bank, .. } => {
557                let b = bank as usize;
558                if state[b] != BankState::Idle {
559                    return Err(format!("cmd {i}: ACT on open bank {bank}"));
560                }
561                if c.at < idle_ready[b] {
562                    return Err(format!(
563                        "cmd {i}: ACT bank {bank} at {} violates tRP/tRFC (idle at {})",
564                        c.at, idle_ready[b]
565                    ));
566                }
567                if c.at < act_ready[b] {
568                    return Err(format!("cmd {i}: ACT bank {bank} violates tRC"));
569                }
570                if let Some(la) = last_act {
571                    if c.at < la + t.trrd {
572                        return Err(format!("cmd {i}: ACT violates tRRD"));
573                    }
574                }
575                let win = c.at.saturating_sub(t.tfaw - 1);
576                if acts.iter().filter(|&&a| a >= win).count() >= 4 {
577                    return Err(format!("cmd {i}: 5th ACT inside tFAW window"));
578                }
579                acts.push(c.at);
580                let horizon = c.at.saturating_sub(4 * t.tfaw);
581                acts.retain(|&a| a >= horizon);
582                last_act = Some(c.at);
583                state[b] = BankState::Active;
584                act_at[b] = c.at;
585                act_ready[b] = c.at + t.trc;
586            }
587            DramCmd::Rd { bank } | DramCmd::Wr { bank } => {
588                let b = bank as usize;
589                let write = matches!(c.cmd, DramCmd::Wr { .. });
590                if state[b] != BankState::Active {
591                    return Err(format!("cmd {i}: CAS on idle bank {bank}"));
592                }
593                if c.at < act_at[b] + t.trcd {
594                    return Err(format!("cmd {i}: CAS violates tRCD on bank {bank}"));
595                }
596                let turn = if bus_last_dir.is_some_and(|d| d != write) {
597                    t.tturn
598                } else {
599                    0
600                };
601                if c.at < bus_busy_until + turn {
602                    return Err(format!(
603                        "cmd {i}: data-bus overlap/turnaround (free at {}, CAS at {})",
604                        bus_busy_until + turn,
605                        c.at
606                    ));
607                }
608                bus_busy_until = c.at + t.data_cycles.max(t.tccd);
609                bus_last_dir = Some(write);
610                last_cas_end[b] = c.at + t.data_cycles;
611                if write {
612                    last_write_end[b] = c.at + t.data_cycles;
613                }
614            }
615            DramCmd::Pre { bank } => {
616                let b = bank as usize;
617                if state[b] != BankState::Active {
618                    return Err(format!("cmd {i}: PRE on idle bank {bank}"));
619                }
620                if c.at < act_at[b] + t.tras {
621                    return Err(format!("cmd {i}: PRE violates tRAS on bank {bank}"));
622                }
623                if c.at < last_cas_end[b] {
624                    return Err(format!(
625                        "cmd {i}: PRE before burst completes on bank {bank}"
626                    ));
627                }
628                if last_write_end[b] > 0 && c.at < last_write_end[b] + t.twr {
629                    return Err(format!("cmd {i}: PRE violates tWR on bank {bank}"));
630                }
631                state[b] = BankState::Idle;
632                idle_ready[b] = idle_ready[b].max(c.at + t.trp);
633            }
634            DramCmd::RefPb { bank } => {
635                let b = bank as usize;
636                if state[b] != BankState::Idle {
637                    return Err(format!("cmd {i}: REFpb on open bank {bank}"));
638                }
639                if c.at < idle_ready[b] {
640                    return Err(format!(
641                        "cmd {i}: REFpb bank {bank} at {} before idle-ready {}",
642                        c.at, idle_ready[b]
643                    ));
644                }
645                refs[b].push(c.at);
646                idle_ready[b] = c.at + t.trfc_pb;
647            }
648        }
649    }
650
651    // Refresh deadlines: over a span long enough to owe refreshes, every bank must
652    // be refreshed, with no gap (start → first, k → k+1) exceeding 1.5× the
653    // effective interval — the staggered background plan keeps well inside this.
654    let span = plan.iter().map(|c| c.at).max().unwrap_or(0);
655    let trefi = t.trefi_effective();
656    if span > 2 * trefi {
657        for (b, bank_refs) in refs.iter().enumerate() {
658            if bank_refs.is_empty() {
659                return Err(format!("bank {b}: no REFpb inside a {span}-cycle plan"));
660            }
661            let mut prev = 0u64;
662            for &r in bank_refs {
663                if r.saturating_sub(prev) > trefi + trefi / 2 {
664                    return Err(format!(
665                        "bank {b}: refresh gap {} exceeds 1.5×tREFI ({})",
666                        r - prev,
667                        trefi + trefi / 2
668                    ));
669                }
670                prev = r;
671            }
672            if span.saturating_sub(prev) > trefi + trefi / 2 {
673                return Err(format!("bank {b}: trailing refresh gap exceeds 1.5×tREFI"));
674            }
675        }
676    }
677    Ok(())
678}
679
680/// A bank-state DEVICE that executes the plan and reports each data burst's
681/// completion cycle. The zero-jitter fact: these times equal the plan's own
682/// (`cas.at + data_cycles`) exactly — the device adds nothing and guesses nothing.
683pub struct DeviceModel;
684
685impl DeviceModel {
686    pub fn execute(t: &DramTimings, plan: &[TimedCmd]) -> Result<Vec<u64>, String> {
687        check_legal(t, plan)?;
688        Ok(plan
689            .iter()
690            .filter_map(|c| match c.cmd {
691                DramCmd::Rd { .. } | DramCmd::Wr { .. } => Some(c.at + t.data_cycles),
692                _ => None,
693            })
694            .collect())
695    }
696}
697
698/// Planned data-beat completion times straight from the plan (what the compiler
699/// promises); [`DeviceModel::execute`] is what the device delivers.
700pub fn planned_beats(t: &DramTimings, plan: &[TimedCmd]) -> Vec<u64> {
701    plan.iter()
702        .filter_map(|c| match c.cmd {
703            DramCmd::Rd { .. } | DramCmd::Wr { .. } => Some(c.at + t.data_cycles),
704            _ => None,
705        })
706        .collect()
707}
708
709/// The standard measurement scenarios — kernel-shaped traffic classes, each checked
710/// legal and measured. Returns a stable JSON document (pure function of the timing
711/// table; no clocks, no randomness) — the `benchmarks/results/dram_sim.json` artifact.
712pub fn standard_report() -> String {
713    let t = DramTimings::lpddr5x();
714    let seq = |base: u64, bytes: u64, write: bool| StreamDesc {
715        base,
716        elem_bytes: 8,
717        stride_bytes: 8,
718        count: bytes / 8,
719        write,
720    };
721
722    let mut scenarios: Vec<(&str, Vec<TimedCmd>, &DramTimings)> = Vec::new();
723    let seq_plan = schedule(&t, &[seq(0, 1 << 20, false)], &[], &[]);
724    scenarios.push(("sequential_read_1mb", seq_plan, &t));
725
726    // nbody-shaped SoA: seven parallel float arrays walked together, row-tiled.
727    let soa: Vec<StreamDesc> = (0..7)
728        .map(|i| seq(i * (1 << 22), 1 << 17, i == 6))
729        .collect();
730    let soa_plan = schedule_ordered(&t, &soa, &[], &[], VisitOrder::RowTiled);
731    scenarios.push(("soa7_rowtiled_nbody", soa_plan, &t));
732
733    // mergesort-shaped: two sorted inputs streamed against one output, row-tiled.
734    let merge = vec![
735        seq(0, 1 << 18, false),
736        seq(1 << 22, 1 << 18, false),
737        seq(1 << 23, 1 << 19, true),
738    ];
739    let merge_plan = schedule_ordered(&t, &merge, &[], &[], VisitOrder::RowTiled);
740    scenarios.push(("merge3_rowtiled", merge_plan, &t));
741
742    // The dynamic tail: a seeded scattered gather (graph_bfs's 15%).
743    let mut x = 0x9E3779B97F4A7C15u64;
744    let addrs: Vec<u64> = (0..4096)
745        .map(|_| {
746            x ^= x << 13;
747            x ^= x >> 7;
748            x ^= x << 17;
749            (x % (1 << 26)) & !7
750        })
751        .collect();
752    let gather_plan = schedule(
753        &t,
754        &[],
755        &[Gather {
756            addrs,
757            elem_bytes: 8,
758            write: false,
759        }],
760        &[],
761    );
762    scenarios.push(("gather_4096_random", gather_plan, &t));
763
764    let hot = DramTimings {
765        hot: true,
766        ..DramTimings::lpddr5x()
767    };
768    let hot_plan = schedule(&hot, &[seq(0, 1 << 20, false)], &[], &[]);
769    let hot_owned = hot.clone();
770
771    let mut body = String::new();
772    let emit = |name: &str, plan: &[TimedCmd], tt: &DramTimings, body: &mut String| {
773        check_legal(tt, plan).unwrap_or_else(|e| panic!("{name}: illegal plan: {e}"));
774        let e = measure(tt, plan);
775        body.push_str(&format!(
776            "    \"{name}\": {{ \"efficiency\": {:.4}, \"bursts\": {}, \"activates\": {}, \
777             \"refreshes\": {}, \"span_cycles\": {} }},\n",
778            e.value(),
779            e.bursts,
780            e.row_activates,
781            e.refreshes,
782            e.span_cycles
783        ));
784    };
785    for (name, plan, tt) in &scenarios {
786        emit(name, plan, tt, &mut body);
787    }
788    emit("sequential_read_1mb_hot", &hot_plan, &hot_owned, &mut body);
789    let body = body.trim_end_matches(",\n").to_string();
790
791    format!(
792        "{{\n  \"meta\": {{\n    \"model\": \"LPDDR5X-class 16-bank closed-page, \
793         bank-rotated, background per-bank refresh\",\n    \"timings\": {{ \"trcd\": {}, \
794         \"trp\": {}, \"tras\": {}, \"trc\": {}, \"trrd\": {}, \"tfaw\": {}, \"tccd\": {}, \
795         \"trfc_pb\": {}, \"trefi_pb\": {}, \"twr\": {}, \"tturn\": {}, \"banks\": {}, \"row_bytes\": {}, \
796         \"burst_bytes\": {} }},\n    \"note\": \"pure function of the timing table — \
797         regenerate with DRAM_RECORD=1 cargo test dram_sim_artifact\", \"unmodeled\": \"bank-group tCCD_L/S, ZQ calibration, command-bus contention — flagged for the memory-architect review\"\n  }},\n  \
798         \"scenarios\": {{\n{body}\n  }}\n}}\n",
799        t.trcd,
800        t.trp,
801        t.tras,
802        t.trc,
803        t.trrd,
804        t.tfaw,
805        t.tccd,
806        t.trfc_pb,
807        t.trefi_pb,
808        t.twr,
809        t.tturn,
810        t.banks,
811        t.row_bytes,
812        t.burst_bytes
813    )
814}
815
816// ── Wave 3: the certified command stream + the dumb in-order executor ────────────────
817//
818// Rung 3 of the co-design ladder: the controller collapses to an executor that plays a
819// compile-time command stream with NO reorder logic, NO page-policy guesses, NO refresh
820// juggling — because the plan already contains all of it. The stream is delta-encoded
821// (each word carries its distance from the previous command), the executor is a
822// countdown FSM, and the countdown arithmetic is proven exact by our certified SAT.
823
824/// Encode a timed plan as executor words: `[delta:24][op:3][bank:5][row:32]`.
825/// `delta` = cycles after the PREVIOUS command (first word: cycles after reset).
826/// op: 0=ACT 1=RD 2=WR 3=PRE 4=REFpb.
827pub fn delta_encode(plan: &[TimedCmd]) -> Vec<u64> {
828    let mut out = Vec::with_capacity(plan.len());
829    let mut prev = 0u64;
830    for c in plan {
831        let delta = c.at - prev;
832        assert!(
833            delta < (1 << 24),
834            "command delta {delta} exceeds 24-bit executor field"
835        );
836        prev = c.at;
837        let (op, bank, row) = match c.cmd {
838            DramCmd::Act { bank, row } => (0u64, bank as u64, row),
839            DramCmd::Rd { bank } => (1, bank as u64, 0),
840            DramCmd::Wr { bank } => (2, bank as u64, 0),
841            DramCmd::Pre { bank } => (3, bank as u64, 0),
842            DramCmd::RefPb { bank } => (4, bank as u64, 0),
843        };
844        assert!(row < (1 << 32), "row exceeds 32-bit executor field");
845        out.push((delta << 40) | (op << 37) | (bank << 32) | row);
846    }
847    out
848}
849
850/// The dumb in-order executor: a countdown FSM. It loads the next word, waits exactly
851/// `delta` cycles, strobes the command pins, and acks the word — in arrival order, by
852/// construction (there is no other datapath). The rung-3 controller.
853pub fn emit_dram_executor_verilog() -> String {
854    r#"// dram_executor — the rung-3 in-order command-stream executor (COLD_HARD_FACTS Wave 3).
855// The compile-time plan is delta-encoded ([delta:24][op:3][bank:5][row:32]); this FSM
856// waits each delta and fires the command pins. Commands can only issue in strict word
857// order — there is no queue or scheduling logic in the netlist to do otherwise. The
858// countdown arithmetic is proven exact by our certified SAT (see codegen_sva::dram).
859// cmd_ack is a FIFO-POP pulse asserted the cycle a word is CONSUMED (loaded), not the
860// cycle it later issues — so the producer's read index advances immediately and a
861// zero-delay command can never reload itself (the classic handshake-latency trap).
862module dram_executor(
863  input clk,
864  input rst,
865  input [63:0] cmd_word,
866  input cmd_valid,
867  output reg cmd_ack,
868  output reg issue,
869  output reg [2:0] op,
870  output reg [4:0] bank,
871  output reg [31:0] row
872);
873  reg loaded;
874  reg [23:0] wait_left;
875  reg [2:0] op_l;
876  reg [4:0] bank_l;
877  reg [31:0] row_l;
878  always @(posedge clk) begin
879    if (rst) begin
880      loaded <= 1'b0; wait_left <= 24'd0; issue <= 1'b0; cmd_ack <= 1'b0;
881      op <= 3'd0; bank <= 5'd0; row <= 32'd0; op_l <= 3'd0; bank_l <= 5'd0; row_l <= 32'd0;
882    end else begin
883      issue <= 1'b0;
884      cmd_ack <= 1'b0;
885      if (!loaded) begin
886        if (cmd_valid) begin
887          wait_left <= cmd_word[63:40];
888          op_l <= cmd_word[39:37];
889          bank_l <= cmd_word[36:32];
890          row_l <= cmd_word[31:0];
891          loaded <= 1'b1;
892          cmd_ack <= 1'b1;
893        end
894      end else if (wait_left == 24'd0) begin
895        issue <= 1'b1;
896        op <= op_l; bank <= bank_l; row <= row_l;
897        loaded <= 1'b0;
898      end else begin
899        wait_left <= wait_left - 24'd1;
900      end
901    end
902  end
903endmodule
904"#
905    .to_string()
906}
907
908/// A self-checking testbench: a ROM of REAL plan words (from [`delta_encode`]) drives
909/// the executor; the bench asserts every issue arrives IN ORDER with the exact opcode/
910/// bank/row of its word, and prints PASS/FAIL. (Absolute-time equality to the plan is
911/// the Rust-side lock; the bench pins order + payload + one-issue-per-word in RTL.)
912pub fn emit_dram_executor_testbench(nwords: usize) -> String {
913    format!(
914        r#"// tb_dram_executor — replays a REAL Wave-2 command stream (dram_plan.hex).
915`timescale 1ns/1ps
916module tb_dram_executor;
917  reg clk = 0; always #5 clk = ~clk;
918  reg rst = 1;
919  reg [63:0] rom [0:{last}];
920  integer idx = 0;
921  integer issues = 0;
922  integer errors = 0;
923  wire [63:0] cmd_word = rom[idx];
924  wire cmd_valid = (idx <= {last});
925  wire cmd_ack, issue;
926  wire [2:0] op; wire [4:0] bank; wire [31:0] row;
927  dram_executor dut(.clk(clk), .rst(rst), .cmd_word(cmd_word), .cmd_valid(cmd_valid),
928                    .cmd_ack(cmd_ack), .issue(issue), .op(op), .bank(bank), .row(row));
929  reg [63:0] expect_word;
930  always @(posedge clk) begin
931    if (cmd_ack) idx <= idx + 1;   // pop on CONSUME (load), not issue
932    if (issue) begin
933      expect_word = rom[issues];   // issues arrive strictly in word order
934      if (op !== expect_word[39:37] || bank !== expect_word[36:32] || row !== expect_word[31:0]) begin
935        errors = errors + 1;
936        $display("FAIL: issue %0d payload mismatch", issues);
937      end
938      issues = issues + 1;
939    end
940  end
941  initial begin
942    $readmemh("dram_plan.hex", rom);
943    repeat (4) @(posedge clk);
944    rst = 0;
945    // Run long enough for every delta in the plan slice.
946    repeat (200000) @(posedge clk);
947    if (issues !== {n}) begin
948      $display("FAIL: issued %0d of {n} commands", issues);
949    end else if (errors == 0) begin
950      $display("PASS: %0d commands issued strictly in order with exact payloads", issues);
951    end
952    $finish;
953  end
954endmodule
955"#,
956        last = nwords - 1,
957        n = nwords
958    )
959}
960
961// ── ComputeDRAM: the in-DRAM compute plane (rung 4, off-the-shelf DRAM) ───────────────
962//
963// ComputeDRAM (Gao/Tziantzioulis/Wentzlaff, MICRO 2019) shows UNMODIFIED DRAM computes
964// RowClone (row copy) and bulk bitwise AND/OR/MAJ when `ACT → PRE → ACT` is issued FASTER
965// than JEDEC tRP/tRAS allow: the interrupted precharge leaves multiple rows sharing the
966// bitlines and the sense amps resolve them to the bitwise MAJORITY (AND = MAJ(a,b,0),
967// OR = MAJ(a,b,1) with a preset control row). This is the DRAM execution plane of the unified
968// LOGOS machine: a bulk-bitwise/copy LOGOS op lowers to a `ComputeDramOp`, whose logical result
969// is proven ≡ the VM's bitwise semantics by the `RowStore` oracle below.
970//
971// These command streams are DELIBERATELY timing-illegal — `check_legal` MUST reject them, and
972// that quarantine is a PIT-OF-SUCCESS invariant (a ComputeDRAM stream can never be mistaken for
973// a certified JEDEC-legal plan). The exact effective violation window is chip-specific and is
974// MEASURED on real silicon in Phase 3; `ComputeDramTiming::nominal` is the model's recipe.
975
976/// The sub-JEDEC violation window that triggers the ComputeDRAM effect, in controller cycles.
977#[derive(Clone, Copy, Debug, PartialEq, Eq)]
978pub struct ComputeDramTiming {
979    /// ACT → PRE gap — deliberately below tRAS (opens the row, then interrupts its close).
980    pub t1: u64,
981    /// PRE → ACT gap — deliberately below tRP (interrupts the precharge mid-flight).
982    pub t2: u64,
983}
984
985impl ComputeDramTiming {
986    /// The nominal paper-class recipe (a few cycles). The real window is MEASURED in Phase 3.
987    pub fn nominal() -> Self {
988        ComputeDramTiming { t1: 2, t2: 1 }
989    }
990}
991
992/// A bulk in-DRAM operation over whole rows of one bank. `RowCopy` is RowClone; `And`/`Or`/`Maj`
993/// are the triple-row-activation majority reductions (`And`/`Or` assume a preset control row
994/// established by a prior `RowCopy` — wired in the Phase 5 lowering).
995#[derive(Clone, Copy, Debug, PartialEq, Eq)]
996pub enum ComputeDramOp {
997    RowCopy {
998        bank: u32,
999        src: u64,
1000        dst: u64,
1001    },
1002    And {
1003        bank: u32,
1004        a: u64,
1005        b: u64,
1006        dst: u64,
1007    },
1008    Or {
1009        bank: u32,
1010        a: u64,
1011        b: u64,
1012        dst: u64,
1013    },
1014    Maj {
1015        bank: u32,
1016        a: u64,
1017        b: u64,
1018        c: u64,
1019        dst: u64,
1020    },
1021}
1022
1023impl ComputeDramOp {
1024    pub fn bank(&self) -> u32 {
1025        match *self {
1026            ComputeDramOp::RowCopy { bank, .. }
1027            | ComputeDramOp::And { bank, .. }
1028            | ComputeDramOp::Or { bank, .. }
1029            | ComputeDramOp::Maj { bank, .. } => bank,
1030        }
1031    }
1032
1033    /// The rows activated in rapid succession (the ACT-PRE-ACT cadence walks these in order).
1034    fn activation_rows(&self) -> Vec<u64> {
1035        match *self {
1036            ComputeDramOp::RowCopy { src, dst, .. } => vec![src, dst],
1037            ComputeDramOp::And { a, b, .. } | ComputeDramOp::Or { a, b, .. } => vec![a, b],
1038            ComputeDramOp::Maj { a, b, c, .. } => vec![a, b, c],
1039        }
1040    }
1041
1042    fn is_reduction(&self) -> bool {
1043        !matches!(self, ComputeDramOp::RowCopy { .. })
1044    }
1045}
1046
1047/// Emit the raw, deliberately timing-VIOLATING command stream that triggers `op` on real DRAM:
1048/// an `ACT → PRE → ACT (…)` cadence with sub-`tRAS`/sub-`tRP` gaps. The stream is NEVER
1049/// JEDEC-legal (that is the point) — `check_legal` rejects it by design.
1050pub fn emit_computedram_stream(op: &ComputeDramOp, tv: &ComputeDramTiming) -> Vec<TimedCmd> {
1051    let bank = op.bank();
1052    let rows = op.activation_rows();
1053    let mut out = Vec::new();
1054    let mut at = 0u64;
1055    // Walk the operand rows, opening each and interrupting its precharge with the next ACT —
1056    // the ACT→PRE→ACT charge-sharing cadence, with sub-tRAS (t1) and sub-tRP (t2) gaps.
1057    for (i, row) in rows.iter().enumerate() {
1058        out.push(TimedCmd {
1059            at,
1060            cmd: DramCmd::Act { bank, row: *row },
1061        });
1062        at += tv.t1;
1063        if i + 1 < rows.len() {
1064            out.push(TimedCmd {
1065                at,
1066                cmd: DramCmd::Pre { bank },
1067            });
1068            at += tv.t2;
1069        }
1070    }
1071    // Reductions (And/Or/Maj) sense the resolved majority off the bitlines; RowCopy needs no read.
1072    if op.is_reduction() {
1073        out.push(TimedCmd {
1074            at,
1075            cmd: DramCmd::Rd { bank },
1076        });
1077        at += 1;
1078    }
1079    out.push(TimedCmd {
1080        at,
1081        cmd: DramCmd::Pre { bank },
1082    });
1083    out
1084}
1085
1086/// Bitwise majority of three words — the sense-amp reduction ComputeDRAM performs per bit.
1087pub fn word_majority(a: u64, b: u64, c: u64) -> u64 {
1088    (a & b) | (a & c) | (b & c)
1089}
1090
1091/// A functional model of DRAM rows — the ComputeDRAM ORACLE the real silicon is measured
1092/// against. Each row is a bit vector packed into `words` u64 words.
1093#[derive(Clone, Debug, Default)]
1094pub struct RowStore {
1095    words: usize,
1096    rows: BTreeMap<(u32, u64), Vec<u64>>,
1097}
1098
1099impl RowStore {
1100    pub fn new(words: usize) -> Self {
1101        RowStore {
1102            words,
1103            rows: BTreeMap::new(),
1104        }
1105    }
1106
1107    pub fn write(&mut self, bank: u32, row: u64, data: &[u64]) {
1108        let mut v = vec![0u64; self.words];
1109        for (slot, w) in v.iter_mut().zip(data.iter()) {
1110            *slot = *w;
1111        }
1112        self.rows.insert((bank, row), v);
1113    }
1114
1115    pub fn read(&self, bank: u32, row: u64) -> Vec<u64> {
1116        self.rows
1117            .get(&(bank, row))
1118            .cloned()
1119            .unwrap_or_else(|| vec![0u64; self.words])
1120    }
1121
1122    /// Apply a ComputeDRAM op to the modeled rows (the oracle semantics): RowClone copies a row;
1123    /// And/Or/Maj resolve the per-bit sense-amp majority (And with a 0 control, Or with a 1 control).
1124    pub fn apply(&mut self, op: &ComputeDramOp) {
1125        let (bank, dst, result): (u32, u64, Vec<u64>) = match *op {
1126            ComputeDramOp::RowCopy { bank, src, dst } => (bank, dst, self.read(bank, src)),
1127            ComputeDramOp::And { bank, a, b, dst } => {
1128                let (ra, rb) = (self.read(bank, a), self.read(bank, b));
1129                (
1130                    bank,
1131                    dst,
1132                    ra.iter()
1133                        .zip(&rb)
1134                        .map(|(x, y)| word_majority(*x, *y, 0))
1135                        .collect(),
1136                )
1137            }
1138            ComputeDramOp::Or { bank, a, b, dst } => {
1139                let (ra, rb) = (self.read(bank, a), self.read(bank, b));
1140                (
1141                    bank,
1142                    dst,
1143                    ra.iter()
1144                        .zip(&rb)
1145                        .map(|(x, y)| word_majority(*x, *y, u64::MAX))
1146                        .collect(),
1147                )
1148            }
1149            ComputeDramOp::Maj { bank, a, b, c, dst } => {
1150                let (ra, rb, rc) = (self.read(bank, a), self.read(bank, b), self.read(bank, c));
1151                let r = ra
1152                    .iter()
1153                    .zip(&rb)
1154                    .zip(&rc)
1155                    .map(|((x, y), z)| word_majority(*x, *y, *z))
1156                    .collect();
1157                (bank, dst, r)
1158            }
1159        };
1160        self.write(bank, dst, &result);
1161    }
1162}
1163
1164#[cfg(test)]
1165#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/dram/tests.rs"]
1166mod tests;