From 6f9c857141421ff0e147a75b0b4e813524db3c86 Mon Sep 17 00:00:00 2001 From: daiyongyuan <1533208939@qq.com> Date: Sat, 12 Sep 2026 12:01:11 +0800 Subject: [PATCH 1/3] feat: add P2E bank difftest --- Cargo.lock | 8 + Cargo.toml | 1 + src/main.rs | 81 +++++ src/nodes/bemu/chip/build.rs | 42 ++- src/nodes/bemu/native/ffi.rs | 26 +- src/nodes/bemu/src/emu/inst/32_mset.rs | 52 +++- src/nodes/bemu/src/emu/inst/decode.rs | 5 +- src/nodes/bemu/src/emu/inst/instruction.rs | 137 +++++++-- src/nodes/bemu/src/lib.rs | 16 + src/nodes/lib/bank-hash/src/comparator.rs | 23 ++ src/nodes/lib/rtl-trace/src/bank_digest.rs | 303 +++++++++++++++---- src/nodes/lib/syscall/src/handlers/openat.rs | 37 +++ src/nodes/lib/syscall/src/state.rs | 3 + src/nodes/p2e/Cargo.toml | 1 + src/nodes/p2e/src/lib.rs | 2 +- src/nodes/p2e/src/runner/mod.rs | 2 +- src/nodes/p2e/src/runner/runworkload.rs | 88 +++++- src/simulation/build.rs | 20 +- src/simulation/difftest.rs | 142 +++++++++ src/simulation/mod.rs | 2 + src/simulation/p2e/run.rs | 118 +++++++- src/simulation/run.rs | 10 +- src/simulation/verilator/run.rs | 104 +------ 23 files changed, 1015 insertions(+), 208 deletions(-) create mode 100644 src/simulation/difftest.rs diff --git a/Cargo.lock b/Cargo.lock index d8c105c..0dd6a13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,7 @@ dependencies = [ "bebop-rtl-trace", "bebop-uart", "duct", + "libc", "log", ] @@ -168,6 +169,13 @@ dependencies = [ name = "bebop-rushb" version = "0.1.0" +[[package]] +name = "bebop-syscall" +version = "0.1.0" +dependencies = [ + "once_cell", +] + [[package]] name = "bebop-uart" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 44f9b5f..24a5716 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "src/nodes/lib/rushB", "src/nodes/lib/fd-redirect", "src/nodes/lib/rtl-trace", + "src/nodes/lib/syscall", "src/nodes/lib/uart", "src/nodes/verilator", "src/nodes/p2e", diff --git a/src/main.rs b/src/main.rs index 4c960bd..23d1c95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,8 @@ pub enum BuildTarget { rtl_dir: PathBuf, #[arg(long, value_name = "DIR")] out_dir: PathBuf, + #[arg(long, help = "Build a P2E+BEMU Bank DiffTest executable")] + diff: bool, }, } @@ -137,6 +139,16 @@ pub enum RunTarget { wave: bool, #[arg(long, help = "Start waveform dump from this cycle")] wave_start: Option, + #[arg( + long, + requires = "golden_elf", + help = "Run P2E FPGA with a BEMU Bank DiffTest golden model" + )] + diff: bool, + #[arg(long, value_name = "ELF")] + golden_elf: Option, + #[arg(long, help = "Initialize the BEMU golden model with proxy kernel mode")] + golden_pk: bool, #[arg(long, help = "Enable RTL instruction trace")] itrace: bool, #[arg(long, help = "Enable RTL memory trace")] @@ -162,3 +174,72 @@ fn main() { std::process::exit(1); } } + +#[cfg(test)] +mod tests { + use super::*; + + fn p2e_args() -> Vec<&'static str> { + vec![ + "bebop", + "run", + "p2e", + "--image", + "image.hex", + "--bitstream", + "bitstream.bit", + "--log-dir", + "log", + ] + } + + #[test] + fn parses_p2e_diff_options() { + let mut args = p2e_args(); + args.extend(["--diff", "--golden-elf", "golden.elf", "--golden-pk"]); + let cli = Cli::try_parse_from(args).unwrap(); + let Commands::Run(RunCommand { + target: + RunTarget::P2e { + diff, + golden_elf, + golden_pk, + .. + }, + }) = cli.command + else { + panic!("expected P2E run command"); + }; + assert!(diff); + assert_eq!(golden_elf, Some(PathBuf::from("golden.elf"))); + assert!(golden_pk); + } + + #[test] + fn p2e_diff_requires_golden_elf() { + let mut args = p2e_args(); + args.push("--diff"); + let error = Cli::try_parse_from(args).unwrap_err().to_string(); + assert!(error.contains("--golden-elf ")); + } + + #[test] + fn parses_p2e_without_diff() { + let cli = Cli::try_parse_from(p2e_args()).unwrap(); + let Commands::Run(RunCommand { + target: + RunTarget::P2e { + diff, + golden_elf, + golden_pk, + .. + }, + }) = cli.command + else { + panic!("expected P2E run command"); + }; + assert!(!diff); + assert_eq!(golden_elf, None); + assert!(!golden_pk); + } +} diff --git a/src/nodes/bemu/chip/build.rs b/src/nodes/bemu/chip/build.rs index daa727f..09c452a 100644 --- a/src/nodes/bemu/chip/build.rs +++ b/src/nodes/bemu/chip/build.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")); + let p2e_abi = env::var_os("BEBOP_BEMU_P2E_ABI").is_some(); let dispatch = manifest_dir.join("dispatch.rs"); let pb = manifest_dir .parent() @@ -30,11 +31,48 @@ fn main() { println!("cargo:rerun-if-changed={}", pb.display()); println!("cargo:rerun-if-changed={}", proto.display()); + if p2e_abi { + let cc = env::var("BEBOP_BEMU_CC").expect("BEBOP_BEMU_CC"); + env::set_var("CC", &cc); + env::set_var("CXX", env::var("BEBOP_BEMU_CXX").expect("BEBOP_BEMU_CXX")); + env::set_var("DTC", env::var("BEBOP_BEMU_DTC").expect("BEBOP_BEMU_DTC")); + env::set_var( + "PATH", + format!( + "{}:/usr/bin:/bin", + PathBuf::from(cc).parent().expect("BEBOP_BEMU_CC parent").display() + ), + ); + env::set_var( + "LD_LIBRARY_PATH", + env::var("BEBOP_BEMU_COMPILER_LIBRARY_PATH").expect("BEBOP_BEMU_COMPILER_LIBRARY_PATH"), + ); + for name in [ + "NIX_CFLAGS_COMPILE", + "NIX_CFLAGS_COMPILE_FOR_TARGET", + "NIX_LDFLAGS", + "NIX_LDFLAGS_FOR_TARGET", + "CPATH", + "LIBRARY_PATH", + "C_INCLUDE_PATH", + "CPLUS_INCLUDE_PATH", + "CFLAGS", + "CXXFLAGS", + "LDFLAGS", + ] { + env::remove_var(name); + } + } + let engine = manifest_dir.join("../../../../../../bebop/src/nodes/bemu"); let native_dir = build_support::spike::native_dir(&engine); let spike_dir = native_dir.join("spike"); - let spike_install_dir = out_dir.join("spike_install"); - let spike_build_dir = out_dir.join("spike_build"); + let spike_install_dir = out_dir.join(if p2e_abi { "spike_install_p2e" } else { "spike_install" }); + let spike_build_dir = out_dir.join(if p2e_abi { "spike_build_p2e" } else { "spike_build" }); build_support::spike::build_and_link(&native_dir, &spike_dir, &spike_build_dir, &spike_install_dir); + println!( + "cargo:rustc-env=BEBOP_BEMU_SPIKE_LIB_DIR={}", + spike_install_dir.join("lib").display() + ); build_support::rerun::emit_engine(&engine, &native_dir); } diff --git a/src/nodes/bemu/native/ffi.rs b/src/nodes/bemu/native/ffi.rs index c7fab2f..4b02f16 100644 --- a/src/nodes/bemu/native/ffi.rs +++ b/src/nodes/bemu/native/ffi.rs @@ -41,7 +41,7 @@ pub struct SharedMemory { } struct SharedBankState { - storage: Vec>, + storage: Vec, cfgs: Vec, map: BankMap, virtual_bank_count: usize, @@ -61,7 +61,9 @@ impl SharedMemory { Arc::new(Self { data: std::cell::UnsafeCell::new(vec![0; size]), banks: std::cell::UnsafeCell::new(SharedBankState { - storage: vec![vec![0; shared_bank_size]; shared_physical_bank_count], + storage: (0..shared_physical_bank_count) + .map(|_| inst::instruction::PrivateBank::new(shared_bank_size)) + .collect(), cfgs: vec![BankConfig::default(); core_count * virtual_bank_count], map: BankMap::new(shared_physical_bank_count), virtual_bank_count, @@ -176,7 +178,7 @@ impl std::ops::DerefMut for GuestMemory { struct EmuState { memory: GuestMemory, - banks: Vec>, + banks: Vec, bank_cfgs: Vec, bank_map: BankMap, shared_memory: Option>, @@ -211,7 +213,9 @@ impl EmuState { memory: shared_memory .clone() .map_or_else(|| GuestMemory::Owned(vec![0; MEM_SIZE]), GuestMemory::Shared), - banks: vec![vec![0; bank_size()]; bank_num()], + banks: (0..bank_num()) + .map(|_| inst::instruction::PrivateBank::new(bank_size())) + .collect(), bank_cfgs: vec![BankConfig::default(); virtual_bank_num()], bank_map: BankMap::new(bank_num()), shared_memory, @@ -233,7 +237,7 @@ impl EmuState { fn reset_accel(&mut self) { for b in &mut self.banks { - b.fill(0); + b.reset(); } self.bank_cfgs.fill(BankConfig::default()); self.bank_map = BankMap::new(bank_num()); @@ -253,7 +257,9 @@ impl EmuState { fn new_host() -> Self { Self { memory: GuestMemory::Owned(Vec::new()), - banks: vec![vec![0; bank_size()]; bank_num()], + banks: (0..bank_num()) + .map(|_| inst::instruction::PrivateBank::new(bank_size())) + .collect(), bank_cfgs: vec![BankConfig::default(); virtual_bank_num()], bank_map: BankMap::new(bank_num()), shared_memory: None, @@ -987,7 +993,7 @@ pub extern "C" fn buckyball_exec(state: *mut c_void, funct7: u8, xs1: u64, xs2: vbank_id, group_id, physical_bank_id, - bank_hash(&banks[physical_bank_id]), + bank_hash(&banks[physical_bank_id].canonical_bytes()), ) } else { let shared = shared_memory @@ -1295,6 +1301,10 @@ struct HartInit { } fn load_elf_memory(state: &mut EmuState, elf_path: &str) -> Result { + state.syscall.working_dir = Path::new(elf_path) + .parent() + .expect("BEMU ELF must have a parent directory") + .to_path_buf(); let load = load_elf(elf_path, &mut state.memory, DRAM_BASE)?; let entry = load.entry; let mem_end = DRAM_BASE + state.memory.len() as u64; @@ -1322,7 +1332,9 @@ fn load_elf_memory(state: &mut EmuState, elf_path: &str) -> Result Result<(), String> { let mem_end = DRAM_BASE + state.memory.len() as u64; + let working_dir = state.syscall.working_dir.clone(); state.syscall = SyscallState::new(); + state.syscall.working_dir = working_dir; state.pk_vm = None; set_guest_mappings(&[]); diff --git a/src/nodes/bemu/src/emu/inst/32_mset.rs b/src/nodes/bemu/src/emu/inst/32_mset.rs index 58df7d1..d1ef088 100644 --- a/src/nodes/bemu/src/emu/inst/32_mset.rs +++ b/src/nodes/bemu/src/emu/inst/32_mset.rs @@ -11,7 +11,7 @@ impl Instruction for Mset { fn exec(xs1: u64, xs2: u64, ctx: &mut ExecContext) -> u64 { let bank_id = rs1_b0(xs1); - let (_rows, col, alloc) = xs2_mset(xs2); + let (_rows, col, alloc, clear) = xs2_mset(xs2); let v = bank_id as u32; let groups = col.max(1); @@ -42,7 +42,11 @@ impl Instruction for Mset { } } for p in allocated { - ctx.banks.initialize(p, 0); + if shared_bank { + ctx.banks.initialize(p, 0); + } else { + ctx.banks.allocate(p, clear); + } } *ctx.config_mut(bank_id) = BankConfig { allocated: true, @@ -65,3 +69,47 @@ impl Instruction for Mset { 1 } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::bank::{bank_num, bank_size, BankConfig, BankMap}; + use crate::inst::instruction::{PrivateBank, TrackedBanks}; + + #[test] + fn allocation_preserves_physical_bank_contents() { + crate::config::configure_default(); + let mut memory = Vec::new(); + let mut storage = (0..bank_num()) + .map(|_| PrivateBank::new(bank_size())) + .collect::>(); + for bank in &mut storage { + bank[..].fill(0x5a); + } + let mut configs = vec![BankConfig::default(); bank_num()]; + let mut bank_map = BankMap::new(bank_num()); + let mut deferred = Vec::new(); + let mut mmio = Vec::new(); + let mut barrier = false; + let mut context = ExecContext { + hart_id: 0, + instruction_id: 0, + memory: &mut memory, + banks: TrackedBanks::new(&mut storage, None, 0), + cfgs: &mut configs, + bank_map: &mut bank_map, + shared: None, + deferred_bank_frees: &mut deferred, + mmio_banks: &mut mmio, + barrier_hit: &mut barrier, + }; + + Mset::exec(0, 0x421, &mut context); + assert_eq!(context.bank_map.resolve(0), Some(0)); + assert_eq!(context.banks[0][0], 0x5a); + + Mset::exec(1, 0xc21, &mut context); + assert_eq!(context.bank_map.resolve(1), Some(1)); + assert_eq!(context.banks[1][0], 0); + } +} diff --git a/src/nodes/bemu/src/emu/inst/decode.rs b/src/nodes/bemu/src/emu/inst/decode.rs index f8166c0..dbd0456 100644 --- a/src/nodes/bemu/src/emu/inst/decode.rs +++ b/src/nodes/bemu/src/emu/inst/decode.rs @@ -38,11 +38,12 @@ pub fn xs2_mem_stride(xs2: u64) -> (u64, u64) { } #[inline] -pub fn xs2_mset(xs2: u64) -> (u64, u64, u64) { +pub fn xs2_mset(xs2: u64) -> (u64, u64, u64, bool) { let row = xs2 & 0x1f; let col = (xs2 >> 5) & 0x1f; let alloc = (xs2 >> 10) & 1; - (row, col, alloc) + let clear = (xs2 >> 11) & 1 != 0; + (row, col, alloc, clear) } /// the bank field in the instruction is **vbank_id**; parse it to physical slot index before accessing `banks`. diff --git a/src/nodes/bemu/src/emu/inst/instruction.rs b/src/nodes/bemu/src/emu/inst/instruction.rs index 036a979..f8634b9 100644 --- a/src/nodes/bemu/src/emu/inst/instruction.rs +++ b/src/nodes/bemu/src/emu/inst/instruction.rs @@ -25,7 +25,7 @@ use super::super::bank::{BankConfig, BankMap}; use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet}; -use std::ops::{Index, IndexMut}; +use std::ops::{Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; /// Per-instruction bank access scoreboard used by BEMU Golden Record /// generation. Mutable bank access records an architectural write before the @@ -75,16 +75,104 @@ impl BankScoreboard { } } +pub struct PrivateBank { + bytes: Vec, + initialized: Vec, +} + +impl PrivateBank { + pub fn new(size: usize) -> Self { + Self { + bytes: vec![0; size], + initialized: vec![true; size], + } + } + + pub fn allocate(&mut self, clear: bool) { + if clear { + self.bytes.fill(0); + } + self.initialized.fill(clear); + } + + pub fn initialize(&mut self, value: u8) { + self.bytes.fill(value); + self.initialized.fill(true); + } + + pub fn reset(&mut self) { + self.bytes.fill(0); + self.initialized.fill(true); + } + + pub fn canonical_bytes(&self) -> Vec { + self.bytes + .iter() + .zip(&self.initialized) + .map(|(&byte, &initialized)| if initialized { byte } else { 0 }) + .collect() + } +} + +impl std::ops::Deref for PrivateBank { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.bytes + } +} + +impl Index for PrivateBank { + type Output = u8; + + fn index(&self, index: usize) -> &Self::Output { + &self.bytes[index] + } +} + +impl IndexMut for PrivateBank { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + self.initialized[index] = true; + &mut self.bytes[index] + } +} + +macro_rules! impl_range_index { + ($range:ty) => { + impl Index<$range> for PrivateBank { + type Output = [u8]; + + fn index(&self, index: $range) -> &Self::Output { + &self.bytes[index] + } + } + + impl IndexMut<$range> for PrivateBank { + fn index_mut(&mut self, index: $range) -> &mut Self::Output { + self.initialized[index.clone()].fill(true); + &mut self.bytes[index] + } + } + }; +} + +impl_range_index!(Range); +impl_range_index!(RangeFrom); +impl_range_index!(RangeFull); +impl_range_index!(RangeInclusive); +impl_range_index!(RangeTo); +impl_range_index!(RangeToInclusive); + /// Bank storage wrapper that reports mutable bank access to the scoreboard. pub struct TrackedBanks<'a> { - banks: &'a mut [Vec], - shared_banks: Option<&'a mut [Vec]>, + banks: &'a mut [PrivateBank], + shared_banks: Option<&'a mut [PrivateBank]>, scoreboard: Option<&'a BankScoreboard>, instruction_id: u64, } impl<'a> TrackedBanks<'a> { - pub fn new(banks: &'a mut [Vec], scoreboard: Option<&'a BankScoreboard>, instruction_id: u64) -> Self { + pub fn new(banks: &'a mut [PrivateBank], scoreboard: Option<&'a BankScoreboard>, instruction_id: u64) -> Self { Self { banks, shared_banks: None, @@ -94,8 +182,8 @@ impl<'a> TrackedBanks<'a> { } pub fn with_shared( - banks: &'a mut [Vec], - shared_banks: &'a mut [Vec], + banks: &'a mut [PrivateBank], + shared_banks: &'a mut [PrivateBank], scoreboard: Option<&'a BankScoreboard>, instruction_id: u64, ) -> Self { @@ -117,9 +205,13 @@ impl<'a> TrackedBanks<'a> { } } + pub fn allocate(&mut self, physical_bank_id: usize, clear: bool) { + self.banks[physical_bank_id].allocate(clear); + } + /// Alias-safe access for instructions that read one bank and write a /// different bank. - pub fn read_write(&mut self, read_bank: usize, write_bank: usize) -> (&[u8], &mut [u8]) { + pub fn read_write(&mut self, read_bank: usize, write_bank: usize) -> (&PrivateBank, &mut PrivateBank) { assert_ne!(read_bank, write_bank, "bank read/write pair must be distinct"); self.record_write(write_bank); let private_count = self.banks.len(); @@ -148,23 +240,22 @@ impl<'a> TrackedBanks<'a> { ), } } - /// Storage clearing performed while allocating a bank is configuration /// initialization and does not produce a BankDataWrite record. pub fn initialize(&mut self, physical_bank_id: usize, value: u8) { if physical_bank_id < self.banks.len() { - self.banks[physical_bank_id].fill(value); + self.banks[physical_bank_id].initialize(value); } else { let private_count = self.banks.len(); self.shared_banks .as_deref_mut() .expect("shared bank storage is unavailable")[physical_bank_id - private_count] - .fill(value); + .initialize(value); } } } -fn split_read_write(banks: &mut [Vec], read_bank: usize, write_bank: usize) -> (&[u8], &mut [u8]) { +fn split_read_write(banks: &mut [PrivateBank], read_bank: usize, write_bank: usize) -> (&PrivateBank, &mut PrivateBank) { if read_bank < write_bank { let (left, right) = banks.split_at_mut(write_bank); (&left[read_bank], &mut right[0]) @@ -175,7 +266,7 @@ fn split_read_write(banks: &mut [Vec], read_bank: usize, write_bank: usize) } impl Index for TrackedBanks<'_> { - type Output = Vec; + type Output = PrivateBank; fn index(&self, index: usize) -> &Self::Output { if index < self.banks.len() { @@ -290,12 +381,12 @@ impl ExecContext<'_> { #[cfg(test)] mod tests { - use super::{BankScoreboard, TrackedBanks}; + use super::{BankScoreboard, PrivateBank, TrackedBanks}; use std::collections::BTreeSet; #[test] fn scoreboard_records_idempotent_mutable_access() { - let mut storage = vec![vec![0u8; 4]; 2]; + let mut storage = vec![PrivateBank::new(4), PrivateBank::new(4)]; let scoreboard = BankScoreboard::new(); scoreboard.issue(7); let mut banks = TrackedBanks::new(&mut storage, Some(&scoreboard), 7); @@ -305,16 +396,26 @@ mod tests { } #[test] - fn reads_and_allocation_initialization_do_not_record_writes() { - let mut storage = vec![vec![1u8; 4]; 2]; + fn reads_do_not_record_writes() { + let mut storage = vec![PrivateBank::new(4), PrivateBank::new(4)]; let scoreboard = BankScoreboard::new(); scoreboard.issue(8); - let mut banks = TrackedBanks::new(&mut storage, Some(&scoreboard), 8); + let banks = TrackedBanks::new(&mut storage, Some(&scoreboard), 8); let _ = banks[1][0]; - banks.initialize(0, 0); drop(banks); assert!(scoreboard.complete(8).is_empty()); } + + #[test] + fn canonicalization_does_not_modify_uncleared_bank_storage() { + let mut bank = PrivateBank::new(4); + bank[..].copy_from_slice(&[0x5a, 0x6b, 0x7c, 0x8d]); + bank.allocate(false); + bank[0] = 0x5a; + + assert_eq!(bank.bytes, [0x5a, 0x6b, 0x7c, 0x8d]); + assert_eq!(bank.canonical_bytes(), [0x5a, 0, 0, 0]); + } } /// Instruction trait - all instructions must implement this diff --git a/src/nodes/bemu/src/lib.rs b/src/nodes/bemu/src/lib.rs index 3f00e04..81cafcb 100644 --- a/src/nodes/bemu/src/lib.rs +++ b/src/nodes/bemu/src/lib.rs @@ -19,6 +19,7 @@ mod inst; mod trace; pub use bebop_bemu_profile::{format_report as format_profile_report, print_report as print_profile_report}; +pub use config::configure_default as configure_default_topology; pub use config::{tile_topology, TileTopology}; pub use ffi::SharedMemory; pub use sim::BemuInstance; @@ -27,5 +28,20 @@ pub use trace::TraceConfig; /// Private-bank geometry used by an in-process RTL DiffTest monitor. /// Geometry follows chip.pb baked at build time. pub fn private_bank_geometry() -> (usize, usize) { + config::configure_default(); (config::bank_size(), config::bank_row_bytes()) } + +pub fn spike_library_dir() -> &'static std::path::Path { + std::path::Path::new(env!("BEBOP_BEMU_SPIKE_LIB_DIR")) +} + +#[cfg(test)] +mod tests { + #[test] + fn private_bank_geometry_initializes_topology() { + let (bank_size, row_bytes) = super::private_bank_geometry(); + assert!(bank_size > 0); + assert!(row_bytes > 0); + } +} diff --git a/src/nodes/lib/bank-hash/src/comparator.rs b/src/nodes/lib/bank-hash/src/comparator.rs index 2dd89ff..f7148d0 100644 --- a/src/nodes/lib/bank-hash/src/comparator.rs +++ b/src/nodes/lib/bank-hash/src/comparator.rs @@ -330,4 +330,27 @@ mod tests { assert_eq!(summary.pass, 1); assert_eq!(summary.total(), 1); } + + #[test] + fn streaming_summary_reports_every_result() { + let output = std::env::temp_dir().join(format!("bebop-bank-summary-{}.ndjson", std::process::id())); + let writer = create_compare_writer(&output).unwrap(); + let mut comparator = StreamingComparator::new(writer, output); + for item in [ + record(BankHashSource::Bemu, 1, 1, 0, 1, b"same"), + record(BankHashSource::Rtl, 1, 1, 0, 9, b"same"), + record(BankHashSource::Bemu, 2, 2, 0, 2, b"golden"), + record(BankHashSource::Rtl, 2, 2, 0, 8, b"rtl"), + record(BankHashSource::Bemu, 3, 3, 0, 3, b"missing"), + record(BankHashSource::Rtl, 4, 4, 0, 7, b"unexpected"), + ] { + comparator.ingest(item).unwrap(); + } + let summary = comparator.finish().unwrap(); + assert_eq!(summary.pass, 1); + assert_eq!(summary.mismatch, 1); + assert_eq!(summary.missing_rtl, 1); + assert_eq!(summary.unexpected_rtl, 1); + assert!(!summary.passed()); + } } diff --git a/src/nodes/lib/rtl-trace/src/bank_digest.rs b/src/nodes/lib/rtl-trace/src/bank_digest.rs index 0580755..83d5a6b 100644 --- a/src/nodes/lib/rtl-trace/src/bank_digest.rs +++ b/src/nodes/lib/rtl-trace/src/bank_digest.rs @@ -63,6 +63,7 @@ struct BankUpdate { #[derive(Clone, Debug)] struct Producer { meta: InstructionMeta, + logical_generations: BTreeMap, completed_at_poll: Option, updates: BTreeMap, } @@ -70,6 +71,8 @@ struct Producer { struct M4Monitor { config: BankDigestConfig, physical_banks: BTreeMap>, + logical_generations: BTreeMap, + physical_generations: BTreeMap, producers: BTreeMap, // Verilator may invoke the combinational issue DPI before the sequential // instruction-allocation DPI in the same eval. Keep those real write @@ -106,6 +109,8 @@ impl M4Monitor { Ok(Self { config, physical_banks: BTreeMap::new(), + logical_generations: BTreeMap::new(), + physical_generations: BTreeMap::new(), producers: BTreeMap::new(), pending_issues: BTreeMap::new(), pending_arrivals: BTreeMap::new(), @@ -147,6 +152,26 @@ impl M4Monitor { } fn allocate(&mut self, event: &ITraceEvent) { + if self.producers.contains_key(&event.rob_id) { + if let Err(error) = self.emit_stable_updates(true) { + self.fail(error); + return; + } + self.retire_drained_producers(true); + if self.producers.contains_key(&event.rob_id) { + self.fail(format!( + "ROB {} was reused before its Bank-Stable updates drained", + event.rob_id + )); + return; + } + } + + if event.funct == 32 { + let vbank_id = event.rs1 as u32 & 0x3ff; + *self.logical_generations.entry(vbank_id).or_default() += 1; + } + self.next_instruction_id = self.next_instruction_id.wrapping_add(1); let mut producer = Producer { meta: InstructionMeta { @@ -154,6 +179,7 @@ impl M4Monitor { funct7: event.funct, pc: event.pc, }, + logical_generations: self.logical_generations.clone(), completed_at_poll: None, updates: BTreeMap::new(), }; @@ -164,12 +190,7 @@ impl M4Monitor { } } - if self.producers.insert(event.rob_id, producer).is_some() { - self.fail(format!( - "ROB {} was reused before its Bank-Stable updates drained", - event.rob_id - )); - } + self.producers.insert(event.rob_id, producer); if let Some(pending) = self.pending_arrivals.remove(&event.rob_id) { for arrival in pending { self.record_arrival(&arrival); @@ -188,7 +209,6 @@ impl M4Monitor { } fn record_issue(&mut self, event: &MTraceIssueEvent) { - let _hart_id = event.hart_id; if event.is_shared != 0 || self.boot_robs.contains(&event.rob_id) { return; } @@ -202,7 +222,8 @@ impl M4Monitor { .or_default() += 1; return; }; - if producer.updates.entry(bank_id).or_default().emitted { + let update = producer.updates.entry(bank_id).or_default(); + if update.emitted { if self.error.is_none() { self.error = Some(format!( "M4 attribution error: write issue arrived after Stable record for instruction {} bank ({},{})", @@ -211,13 +232,34 @@ impl M4Monitor { } return; } - producer.updates.get_mut(&bank_id).expect("update exists").issued += 1; + update.issued += 1; } fn record_arrival(&mut self, event: &MTraceEvent) { if event.is_write == 0 || event.is_shared != 0 || self.boot_robs.contains(&event.rob_id) { return; } + let ready = self + .producers + .iter() + .filter(|(rob_id, producer)| **rob_id != event.rob_id && producer.completed_at_poll.is_some()) + .flat_map(|(&rob_id, producer)| { + producer.updates.iter().filter_map(move |(&bank_id, update)| { + (!update.emitted + && update.issued != 0 + && update.issued == update.arrived + && update.physical_bank_id == Some(event.pbank_id)) + .then_some((rob_id, bank_id)) + }) + }) + .collect::>(); + for (rob_id, bank_id) in ready { + if let Err(error) = self.emit_update(rob_id, bank_id) { + self.fail(error); + return; + } + } + let bank_id = LogicalBankId::new(event.vbank_id, event.group_id); let instruction_id; { @@ -259,10 +301,25 @@ impl M4Monitor { )); return; } - // Shadow the physical SRAM, not the logical mapping. Reallocating a - // logical bank does not itself prove that RTL cleared the underlying - // storage; preserving the physical contents lets full-bank DiffTest - // expose such initialization bugs and preserves masked-off bytes. + let generation = { + let producer = self.producers.get(&event.rob_id).expect("producer exists"); + producer + .logical_generations + .get(&event.vbank_id) + .copied() + .unwrap_or_default() + }; + let assignment = (event.vbank_id, generation); + if self.physical_generations.get(&event.pbank_id) != Some(&assignment) { + self.physical_banks + .entry(event.pbank_id) + .or_insert_with(|| vec![0; self.config.bank_size]) + .fill(0); + self.physical_generations.insert(event.pbank_id, assignment); + } + // Shadow the physical SRAM, not the logical mapping. A new logical + // assignment starts from canonical zero; later rows in the same + // write transaction preserve masked-off bytes. let bank = self .physical_banks .entry(event.pbank_id) @@ -308,46 +365,50 @@ impl M4Monitor { } for (rob_id, bank_id) in ready { - let producer = self.producers.get(&rob_id).expect("ready producer exists"); - let update = producer.updates.get(&bank_id).expect("ready update exists"); - let meta = producer.meta.clone(); - let physical_bank_id = update.physical_bank_id; - let physical_bank_id = physical_bank_id.expect("arrived update has a physical bank id"); - let bytes = self - .physical_banks - .get(&physical_bank_id) - .expect("arrived update has a physical shadow bank"); - self.next_line = self.next_line.wrapping_add(1); - let record = BankDigestRecord::new( - BankHashSource::Rtl, - meta.instruction_id, - bank_id, - Some(physical_bank_id), - bank_hash(bytes), - meta.funct7, - format!("funct7_{}", meta.funct7), - BankHashEventClass::BankDataWrite, - BankHashTime::Cycle(state::rtl_clk()), - Some(meta.pc), - Some(format!("{RTL_RECORD_FILE}:{}", self.next_line)), - ); - let line = record.to_ndjson().map_err(|error| error.to_string())?; - self.output - .write_all(line.as_bytes()) - .map_err(|error| error.to_string())?; - self.output.flush().map_err(|error| error.to_string())?; - submit_runtime_bank_digest(&record); - self.producers - .get_mut(&rob_id) - .expect("ready producer exists") - .updates - .get_mut(&bank_id) - .expect("ready update exists") - .emitted = true; + self.emit_update(rob_id, bank_id)?; } Ok(()) } + fn emit_update(&mut self, rob_id: u32, bank_id: LogicalBankId) -> Result<(), String> { + let producer = self.producers.get(&rob_id).expect("ready producer exists"); + let update = producer.updates.get(&bank_id).expect("ready update exists"); + let meta = producer.meta.clone(); + let physical_bank_id = update.physical_bank_id.expect("arrived update has a physical bank id"); + let bytes = self + .physical_banks + .get(&physical_bank_id) + .expect("arrived update has a physical shadow bank"); + self.next_line = self.next_line.wrapping_add(1); + let record = BankDigestRecord::new( + BankHashSource::Rtl, + meta.instruction_id, + bank_id, + Some(physical_bank_id), + bank_hash(bytes), + meta.funct7, + format!("funct7_{}", meta.funct7), + BankHashEventClass::BankDataWrite, + BankHashTime::Cycle(state::rtl_clk()), + Some(meta.pc), + Some(format!("{RTL_RECORD_FILE}:{}", self.next_line)), + ); + let line = record.to_ndjson().map_err(|error| error.to_string())?; + self.output + .write_all(line.as_bytes()) + .map_err(|error| error.to_string())?; + self.output.flush().map_err(|error| error.to_string())?; + submit_runtime_bank_digest(&record); + self.producers + .get_mut(&rob_id) + .expect("ready producer exists") + .updates + .get_mut(&bank_id) + .expect("ready update exists") + .emitted = true; + Ok(()) + } + fn retire_drained_producers(&mut self, force: bool) { let poll_count = self.poll_count; self.producers.retain(|_, producer| { @@ -387,6 +448,7 @@ impl M4Monitor { .pending_issues .values() .flat_map(|updates| updates.values()) + .copied() .sum::(), arrived_writes: self .producers @@ -412,10 +474,10 @@ impl M4Monitor { .pending_issues .iter() .flat_map(|(rob_id, updates)| { - updates.iter().map(move |(bank_id, issued)| { + updates.iter().map(move |(bank_id, update)| { format!( "rob={} bank=({},{}) issued={}", - rob_id, bank_id.vbank_id, bank_id.group_id, issued + rob_id, bank_id.vbank_id, bank_id.group_id, update ) }) }) @@ -602,6 +664,33 @@ mod tests { assert!(monitor.status().is_drained()); } + #[test] + fn physical_bank_overwrite_emits_completed_producer_before_new_write() { + let id = TEST_ID.fetch_add(1, Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!("bebop-m3-overwrite-{}-{id}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let mut monitor = M4Monitor::new(&dir, BankDigestConfig::new(64, 16)).unwrap(); + + monitor.record_instruction(&instruction(2, 1, 33)); + monitor.record_issue(&issue(1, 0, 0)); + monitor.record_arrival(&arrival(1, 0, 0, 0, 11)); + monitor.record_instruction(&instruction(0, 1, 33)); + + monitor.record_instruction(&instruction(2, 2, 33)); + monitor.record_issue(&issue(2, 0, 0)); + monitor.record_arrival(&arrival(2, 0, 0, 0, 22)); + + assert_eq!(monitor.next_line, 1); + let mut first_bank = vec![0; 64]; + first_bank[..8].copy_from_slice(&11u64.to_le_bytes()); + let output = std::fs::read_to_string(dir.join(RTL_RECORD_FILE)).unwrap(); + assert!(output.contains(&format!("\"digest_u64\":{}", bank_hash(&first_bank)))); + + monitor.record_instruction(&instruction(0, 2, 33)); + monitor.finish().unwrap(); + assert_eq!(monitor.next_line, 2); + } + #[test] fn boot_rom_writes_do_not_enter_guest_scoreboard() { let mut monitor = test_monitor(); @@ -646,6 +735,114 @@ mod tests { assert!(monitor.status().is_drained()); } + #[test] + fn rob_reuse_flushes_completed_producer_without_host_poll() { + let mut monitor = test_monitor(); + monitor.record_instruction(&instruction(2, 1, 64)); + monitor.record_issue(&issue(1, 5, 0)); + monitor.record_arrival(&arrival(1, 5, 0, 0, 9)); + monitor.record_instruction(&instruction(0, 1, 64)); + + monitor.record_instruction(&instruction(2, 1, 65)); + + assert!(monitor.error.is_none()); + assert_eq!(monitor.next_line, 1); + monitor.record_instruction(&instruction(0, 1, 65)); + monitor.finish().unwrap(); + assert!(monitor.status().is_drained()); + } + + #[test] + fn rob_reuse_with_pending_write_is_an_error() { + let mut monitor = test_monitor(); + monitor.record_instruction(&instruction(2, 1, 64)); + monitor.record_issue(&issue(1, 5, 0)); + monitor.record_instruction(&instruction(0, 1, 64)); + + monitor.record_instruction(&instruction(2, 1, 65)); + + assert_eq!( + monitor.error.as_deref(), + Some("M4 attribution error: ROB 1 was reused before its Bank-Stable updates drained") + ); + } + + #[test] + fn write_issue_generation_survives_later_mset() { + let mut monitor = test_monitor(); + monitor.record_instruction(&instruction(2, 1, 64)); + monitor.record_issue(&issue(1, 5, 0)); + + let mut first = arrival(1, 5, 0, 0, 0x0807_0605_0403_0201); + first.pbank_id = 5; + monitor.record_arrival(&first); + + let mut mset = instruction(2, 2, 32); + mset.rs1 = 5; + monitor.record_instruction(&mset); + + monitor.record_issue(&issue(1, 5, 0)); + let mut second = arrival(1, 5, 0, 1, 0x1817_1615_1413_1211); + second.pbank_id = 5; + monitor.record_arrival(&second); + + assert_eq!( + &monitor.physical_banks[&5][..16], + &[1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0] + ); + assert_eq!( + &monitor.physical_banks[&5][16..32], + &[17, 18, 19, 20, 21, 22, 23, 24, 0, 0, 0, 0, 0, 0, 0, 0] + ); + + monitor.record_instruction(&instruction(0, 1, 64)); + monitor.record_instruction(&instruction(0, 2, 32)); + monitor.finish().unwrap(); + } + + #[test] + fn pending_write_callbacks_use_producer_generation() { + let id = TEST_ID.fetch_add(1, Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!("bebop-m3-arrival-generation-{}-{id}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let mut monitor = M4Monitor::new(&dir, BankDigestConfig::new(64, 16)).unwrap(); + + monitor.record_instruction(&instruction(2, 9, 64)); + monitor.record_issue(&issue(9, 5, 0)); + let mut stale = arrival(9, 5, 0, 2, 0x2827_2625_2423_2221); + stale.pbank_id = 5; + monitor.record_arrival(&stale); + monitor.record_instruction(&instruction(0, 9, 64)); + monitor.poll(false).unwrap(); + + monitor.record_issue(&issue(1, 5, 0)); + let mut first = arrival(1, 5, 0, 0, 0x0807_0605_0403_0201); + first.pbank_id = 5; + monitor.record_arrival(&first); + + let mut mset = instruction(2, 2, 32); + mset.rs1 = 5; + monitor.record_instruction(&mset); + + monitor.record_instruction(&instruction(2, 1, 64)); + monitor.record_issue(&issue(1, 5, 0)); + let mut second = arrival(1, 5, 0, 1, 0x1817_1615_1413_1211); + second.pbank_id = 5; + monitor.record_arrival(&second); + + assert_eq!(monitor.physical_generations[&5], (5, 1)); + assert_eq!(&monitor.physical_banks[&5][32..40], &[0; 8]); + monitor.record_instruction(&instruction(0, 1, 64)); + monitor.record_instruction(&instruction(0, 2, 32)); + monitor.finish().unwrap(); + + let mut expected = vec![0; 64]; + expected[..8].copy_from_slice(&0x0807_0605_0403_0201u64.to_le_bytes()); + expected[16..24].copy_from_slice(&0x1817_1615_1413_1211u64.to_le_bytes()); + let output = std::fs::read_to_string(dir.join(RTL_RECORD_FILE)).unwrap(); + assert!(output.contains(&format!("\"digest_u64\":{}", bank_hash(&expected)))); + } + #[test] fn missing_arrival_is_reported_at_finish() { let mut monitor = test_monitor(); @@ -690,7 +887,7 @@ mod tests { } #[test] - fn physical_bank_reuse_preserves_unwritten_bytes() { + fn physical_bank_reuse_canonicalizes_unwritten_bytes() { let mut monitor = test_monitor(); monitor.record_instruction(&instruction(2, 11, 64)); monitor.record_issue(&issue(11, 5, 0)); @@ -705,6 +902,6 @@ mod tests { reused.write_mask = 0x0001; monitor.record_arrival(&reused); - assert_eq!(&monitor.physical_banks[&3][..8], &[0xff, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(&monitor.physical_banks[&3][..8], &[0xff, 0, 0, 0, 0, 0, 0, 0]); } } diff --git a/src/nodes/lib/syscall/src/handlers/openat.rs b/src/nodes/lib/syscall/src/handlers/openat.rs index 7865923..01e8880 100644 --- a/src/nodes/lib/syscall/src/handlers/openat.rs +++ b/src/nodes/lib/syscall/src/handlers/openat.rs @@ -1,6 +1,7 @@ use crate::state::SyscallState; use crate::utils::guest_cstr; use std::fs::OpenOptions; +use std::path::Path; pub fn handle_openat( state: &mut SyscallState, @@ -39,6 +40,13 @@ pub fn handle_openat( opts.read(true); } + let path = Path::new(path); + let path = if path.is_absolute() { + path.to_path_buf() + } else { + state.working_dir.join(path) + }; + match opts.open(path) { Ok(file) => { let fd = state.alloc_fd(file); @@ -47,3 +55,32 @@ pub fn handle_openat( Err(_) => ((-1i64 as u64), false), } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::GUEST_MEM_BASE; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn relative_path_uses_guest_working_directory() { + let dir = std::env::temp_dir().join(format!( + "bebop-syscall-openat-{}-{}", + std::process::id(), + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() + )); + std::fs::create_dir(&dir).unwrap(); + std::fs::write(dir.join("payload.bin"), b"payload").unwrap(); + + let mut state = SyscallState::new(); + state.working_dir = dir.clone(); + let mut memory = vec![0; 32]; + memory[..12].copy_from_slice(b"payload.bin\0"); + + let (fd, should_exit) = handle_openat(&mut state, -100, GUEST_MEM_BASE, 0, 0, &memory); + + assert_eq!(fd, 3); + assert!(!should_exit); + std::fs::remove_dir_all(dir).unwrap(); + } +} diff --git a/src/nodes/lib/syscall/src/state.rs b/src/nodes/lib/syscall/src/state.rs index c8dc294..49ea992 100644 --- a/src/nodes/lib/syscall/src/state.rs +++ b/src/nodes/lib/syscall/src/state.rs @@ -1,6 +1,7 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use std::fs::File; +use std::path::PathBuf; use std::sync::Mutex; pub static SYSCALL_STATE: Lazy> = Lazy::new(|| Mutex::new(SyscallState::new())); @@ -13,6 +14,7 @@ pub struct SyscallState { pub mmap_base: u64, pub mem_low: u64, pub mem_high: u64, + pub working_dir: PathBuf, } impl SyscallState { @@ -25,6 +27,7 @@ impl SyscallState { mmap_base: 0, mem_low: crate::constants::GUEST_MEM_BASE, mem_high: 0, + working_dir: PathBuf::new(), } } diff --git a/src/nodes/p2e/Cargo.toml b/src/nodes/p2e/Cargo.toml index 4a2e9a6..b6ae4be 100644 --- a/src/nodes/p2e/Cargo.toml +++ b/src/nodes/p2e/Cargo.toml @@ -13,6 +13,7 @@ bebop-rtl-trace = { path = "../lib/rtl-trace" } bebop-uart = { path = "../lib/uart" } log = "0.4" duct = "0.13" +libc = "0.2" [build-dependencies] duct = "0.13" diff --git a/src/nodes/p2e/src/lib.rs b/src/nodes/p2e/src/lib.rs index e873a3e..b89dedd 100644 --- a/src/nodes/p2e/src/lib.rs +++ b/src/nodes/p2e/src/lib.rs @@ -6,7 +6,7 @@ pub use builder::BitstreamBuilder; pub use ctb::ffi; pub use runner::{ configure_vvac_environment, generate_main_tcl, init_ctb, source_environment, start_vdbg_background, - wait_for_completion, wait_for_flash, FlashBitstreamStep, InitStep, RunWorkloadStep, SimulationResult, + wait_for_completion, wait_for_flash, FlashBitstreamStep, InitStep, RunWorkloadStep, SimulationResult, VdbgProcess, }; pub type Result = std::result::Result; diff --git a/src/nodes/p2e/src/runner/mod.rs b/src/nodes/p2e/src/runner/mod.rs index f830246..ce75ce5 100644 --- a/src/nodes/p2e/src/runner/mod.rs +++ b/src/nodes/p2e/src/runner/mod.rs @@ -2,7 +2,7 @@ mod runworkload; pub use runworkload::{ configure_vvac_environment, generate_main_tcl, init_ctb, source_environment, start_vdbg_background, - wait_for_completion, wait_for_flash, SimulationResult, + wait_for_completion, wait_for_flash, SimulationResult, VdbgProcess, }; // Runner steps - use path attribute to map module names to directories with numeric prefixes diff --git a/src/nodes/p2e/src/runner/runworkload.rs b/src/nodes/p2e/src/runner/runworkload.rs index 41917a7..5cd74bc 100644 --- a/src/nodes/p2e/src/runner/runworkload.rs +++ b/src/nodes/p2e/src/runner/runworkload.rs @@ -1,5 +1,6 @@ use crate::ffi::{self, CtbManager}; -use std::path::Path; +use std::path::{Path, PathBuf}; +use std::process::Child; use std::time::{Duration, Instant}; #[derive(Debug, Clone)] @@ -35,11 +36,12 @@ pub fn init_ctb(case_home: &Path, rtcfg_path: &Path) -> Result Result { +pub fn wait_for_completion(mut poll: impl FnMut() -> Result<(), String>) -> Result { let started = Instant::now(); let poll_interval = Duration::from_millis(100); loop { + poll()?; if ffi::check_exit() { let exit_code = ffi::exit_code(); let uart_log = ffi::uart_log(); @@ -126,7 +128,34 @@ exit Ok(tcl) } -pub fn start_vdbg_background(tcl_path: &Path) -> Result<(), String> { +pub struct VdbgProcess { + child: Child, + exit_flag: Option, +} + +impl Drop for VdbgProcess { + fn drop(&mut self) { + if let Some(exit_flag) = &self.exit_flag { + std::fs::write(exit_flag, "").expect("failed to signal vdbg exit"); + return; + } + let process_group = i32::try_from(self.child.id()).expect("vdbg PID must fit pid_t"); + unsafe { + libc::kill(-process_group, libc::SIGKILL); + } + let _ = self.child.wait(); + } +} + +impl VdbgProcess { + pub fn exit_on_drop(mut self, exit_flag: PathBuf) -> Self { + self.exit_flag = Some(exit_flag); + self + } +} + +pub fn start_vdbg_background(tcl_path: &Path) -> Result { + use std::os::unix::process::CommandExt; use std::process::Command; let sourceme = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("sourceme.sh"); @@ -134,18 +163,20 @@ pub fn start_vdbg_background(tcl_path: &Path) -> Result<(), String> { return Err(format!("sourceme.sh not found: {}", sourceme.display())); } - let command = format!("source {} && vdbg {} &", sourceme.display(), tcl_path.display()); - - log::info!("Starting vdbg in background: {}", command); + log::info!("Starting vdbg: {}", tcl_path.display()); - Command::new("bash") + let child = Command::new("bash") .arg("-c") - .arg(&command) + .arg("source \"$1\" && exec vdbg \"$2\"") + .arg("bash") + .arg(&sourceme) + .arg(tcl_path) .env_remove("LD_PRELOAD") + .process_group(0) .spawn() .map_err(|e| format!("Failed to start vdbg: {}", e))?; - Ok(()) + Ok(VdbgProcess { child, exit_flag: None }) } pub fn source_environment() -> Result<(), String> { @@ -188,8 +219,45 @@ pub fn configure_vvac_environment() { log::info!("Running P2E in onboard mode"); } -pub fn wait_for_flash(flash_done_flag: &Path) { +pub fn wait_for_flash(flash_done_flag: &Path, mut poll: impl FnMut() -> Result<(), String>) -> Result<(), String> { while !flash_done_flag.exists() { + poll()?; std::thread::sleep(Duration::from_millis(100)); } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::VdbgProcess; + use std::os::unix::process::CommandExt; + use std::process::Command; + + #[test] + fn vdbg_process_drop_kills_its_process_group() { + let child = Command::new("bash") + .arg("-c") + .arg("sleep 30 & wait") + .process_group(0) + .spawn() + .unwrap(); + let process_group = i32::try_from(child.id()).unwrap(); + + drop(VdbgProcess { child, exit_flag: None }); + + assert_eq!(unsafe { libc::kill(-process_group, 0) }, -1); + assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ESRCH)); + } + + #[test] + fn vdbg_process_after_ctb_signals_tcl_exit() { + let exit_flag = std::env::temp_dir().join(format!("bebop-p2e-exit-{}", std::process::id())); + let _ = std::fs::remove_file(&exit_flag); + let child = Command::new("true").spawn().unwrap(); + + drop(VdbgProcess { child, exit_flag: None }.exit_on_drop(exit_flag.clone())); + + assert!(exit_flag.is_file()); + std::fs::remove_file(exit_flag).unwrap(); + } } diff --git a/src/simulation/build.rs b/src/simulation/build.rs index c901d60..b4f2999 100644 --- a/src/simulation/build.rs +++ b/src/simulation/build.rs @@ -40,7 +40,7 @@ pub fn build(command: BuildCommand) -> Result<(), Whatever> { println!("Built executable: {}", dest.display()); Ok(()) } - BuildTarget::P2e { rtl_dir, out_dir } => { + BuildTarget::P2e { rtl_dir, out_dir, diff } => { if !rtl_dir.is_dir() { let message = format!("RTL directory does not exist: {}", rtl_dir.display()); return Err(Whatever::without_source(message)); @@ -50,7 +50,7 @@ pub fn build(command: BuildCommand) -> Result<(), Whatever> { .whatever_context("failed to canonicalize RTL directory")?; std::fs::create_dir_all(&out_dir).whatever_context("failed to create output directory")?; println!("Building p2e: {} -> {}", rtl_dir.display(), out_dir.display()); - let features = "p2e"; + let features = if diff { "p2e,bemu,difftest" } else { "p2e" }; cmd!("cargo", "build", "--release", "--bin", "bebop", "--features", features) .env("VSRC_PATH", &rtl_dir) .env("OUT_PATH", &out_dir) @@ -71,7 +71,21 @@ pub fn build(command: BuildCommand) -> Result<(), Whatever> { // copy the built executable to the output directory let dest = out_dir.join("bebop-p2e"); - std::fs::copy("target/release/bebop", &dest).whatever_context("failed to copy built executable")?; + let staged = out_dir.join(".bebop-p2e.new"); + #[cfg(feature = "bemu")] + if diff { + std::fs::copy( + bebop_bemu::spike_library_dir().join("libriscv.so"), + out_dir.join("libriscv.so"), + ) + .whatever_context("failed to install libriscv.so")?; + } + let executable = std::path::PathBuf::from( + std::env::var_os("CARGO_TARGET_DIR").expect("CARGO_TARGET_DIR must be set for P2E builds"), + ) + .join("release/bebop"); + std::fs::copy(executable, &staged).whatever_context("failed to stage built executable")?; + std::fs::rename(staged, &dest).whatever_context("failed to install built executable")?; println!("Built P2E runtime: {}", dest.display()); Ok(()) } diff --git a/src/simulation/difftest.rs b/src/simulation/difftest.rs new file mode 100644 index 0000000..ea3bcd9 --- /dev/null +++ b/src/simulation/difftest.rs @@ -0,0 +1,142 @@ +use bebop_bank_hash::{ + init_runtime_packet_channel, run_online_compare_with_summary, runtime_packet_status, + shutdown_runtime_packet_channel, BankHashCompareSummary, +}; +use bebop_bemu::{BemuInstance, TraceConfig as BemuTraceConfig}; +use snafu::{FromString, ResultExt, Whatever}; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; + +pub struct DiffSession { + golden: Option, + golden_worker: Option>>, + compare_worker: Option>>, + cancel_golden: Arc, +} + +impl DiffSession { + pub fn new(elf: &Path, log_dir: &Path, pk: bool) -> Result { + let receiver = init_runtime_packet_channel(); + let output = log_dir.join("bank_diff.ndjson"); + let compare_worker = std::thread::Builder::new() + .name("bank-diff-m4".to_string()) + .spawn(move || run_online_compare_with_summary(receiver, output).map_err(|error| error.to_string())) + .map_err(|error| { + shutdown_runtime_packet_channel(); + Whatever::without_source(format!("failed to start Bank DiffTest M4 worker: {error}")) + })?; + + let golden_result = (|| { + let golden_log_dir = log_dir.join("golden"); + let mut trace = BemuTraceConfig::new(false, false); + trace.btrace = true; + let mut golden = BemuInstance::new(&golden_log_dir, trace, false, false) + .whatever_context("failed to create BEMU Golden Model")?; + golden.load_elf(elf)?; + golden.init_hart(pk)?; + Ok::<_, Whatever>(golden) + })(); + let golden = match golden_result { + Ok(golden) => golden, + Err(error) => { + shutdown_runtime_packet_channel(); + let _ = compare_worker.join(); + return Err(error); + } + }; + + Ok(Self { + golden: Some(golden), + golden_worker: None, + compare_worker: Some(compare_worker), + cancel_golden: Arc::new(AtomicBool::new(false)), + }) + } + + #[cfg(feature = "verilator")] + pub fn step_golden(&mut self) -> Result<(), Whatever> { + let golden = self.golden.as_mut().expect("foreground golden exists"); + if !golden.finished() { + golden.step()?; + } + Ok(()) + } + + pub fn start_golden_background(&mut self) -> Result<(), Whatever> { + let mut golden = self.golden.take().expect("foreground golden exists"); + let cancel = Arc::clone(&self.cancel_golden); + self.golden_worker = Some( + std::thread::Builder::new() + .name("bemu-golden".to_string()) + .spawn(move || { + bebop_bemu::configure_default_topology(); + while !golden.finished() && !cancel.load(Ordering::Relaxed) { + golden.step().map_err(|error| error.to_string())?; + } + if cancel.load(Ordering::Relaxed) { + return Ok(()); + } + let code = golden.exit_code().unwrap_or(0); + if code != 0 { + return Err(format!("BEMU Golden Model exited with code {code}")); + } + Ok(()) + }) + .map_err(|error| Whatever::without_source(format!("failed to start BEMU Golden Model: {error}")))?, + ); + Ok(()) + } + + pub fn finish_golden(&mut self) -> Result<(), Whatever> { + if let Some(worker) = self.golden_worker.take() { + return worker + .join() + .map_err(|_| Whatever::without_source("BEMU Golden Model worker panicked".to_string()))? + .map_err(Whatever::without_source); + } + + let golden = self.golden.as_mut().expect("foreground golden exists"); + while !golden.finished() { + golden.step()?; + } + let code = golden.exit_code().unwrap_or(0); + if code != 0 { + return Err(Whatever::without_source(format!( + "BEMU Golden Model exited with code {code}" + ))); + } + Ok(()) + } + + pub fn finish(mut self) -> Result { + let packet_status = runtime_packet_status(); + shutdown_runtime_packet_channel(); + let summary = self + .compare_worker + .take() + .expect("DiffTest worker exists") + .join() + .map_err(|_| Whatever::without_source("Bank DiffTest M4 worker panicked".to_string()))? + .map_err(Whatever::without_source)?; + println!( + "Bank DiffTest runtime packets: submitted={} no_sink={} send_failed={}", + packet_status.submitted, packet_status.no_sink, packet_status.send_failed + ); + Ok(summary) + } +} + +impl Drop for DiffSession { + fn drop(&mut self) { + self.cancel_golden.store(true, Ordering::Relaxed); + shutdown_runtime_packet_channel(); + if let Some(worker) = self.golden_worker.take() { + let _ = worker.join(); + } + if let Some(worker) = self.compare_worker.take() { + let _ = worker.join(); + } + } +} diff --git a/src/simulation/mod.rs b/src/simulation/mod.rs index 3602e9c..ab740a8 100644 --- a/src/simulation/mod.rs +++ b/src/simulation/mod.rs @@ -1,5 +1,7 @@ pub mod bemu; pub mod build; +#[cfg(feature = "bemu")] +pub mod difftest; pub mod p2e; pub mod run; pub mod verilator; diff --git a/src/simulation/p2e/run.rs b/src/simulation/p2e/run.rs index ae1bb58..085f400 100644 --- a/src/simulation/p2e/run.rs +++ b/src/simulation/p2e/run.rs @@ -1,9 +1,15 @@ -use snafu::{FromString, Whatever}; +#[cfg(feature = "p2e")] +use snafu::FromString; +use snafu::Whatever; #[cfg(feature = "p2e")] use std::path::PathBuf; +#[cfg(feature = "p2e")] +use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(feature = "p2e")] use bebop_p2e::{self}; +#[cfg(all(feature = "p2e", feature = "bemu"))] +use bebop_rtl_trace::{finish_bank_digest, poll_bank_digest, BankDigestConfig}; #[cfg(feature = "p2e")] use bebop_rtl_trace::{init_trace, write_trace_summary, TraceConfig}; #[cfg(feature = "p2e")] @@ -11,9 +17,15 @@ use bebop_uart::{ConsoleConfig, ConsoleServer}; #[cfg(feature = "p2e")] use snafu::ResultExt; +#[cfg(all(feature = "p2e", feature = "bemu"))] +use crate::simulation::difftest::DiffSession; + #[cfg(feature = "p2e")] const FPGA_LOCATION: &str = "0.A"; +#[cfg(feature = "p2e")] +static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); + #[cfg(feature = "p2e")] pub struct P2eRunConfig { pub image: PathBuf, @@ -22,6 +34,9 @@ pub struct P2eRunConfig { pub multi_fpga: bool, pub wave: bool, pub wave_start: Option, + pub diff: bool, + pub golden_elf: Option, + pub golden_pk: bool, pub trace: P2eTraceConfig, } @@ -39,12 +54,29 @@ pub struct P2eTraceConfig { pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).try_init(); + if config.diff && config.golden_elf.is_none() { + return Err(Whatever::without_source( + "P2E --diff requires --golden-elf ".to_string(), + )); + } + #[cfg(not(feature = "bemu"))] + if config.diff { + return Err(Whatever::without_source( + "this executable was built without BEMU; rebuild P2E with --diff".to_string(), + )); + } + if !config.image.exists() { snafu::whatever!("P2E image not found: {}", config.image.display()); } if !config.bitstream.exists() { snafu::whatever!("bitstream not found: {}", config.bitstream.display()); } + if let Some(golden_elf) = config.golden_elf.as_ref().filter(|_| config.diff) { + if !golden_elf.exists() { + snafu::whatever!("BEMU golden ELF not found: {}", golden_elf.display()); + } + } if !config.log_dir.exists() { snafu::whatever!("log directory not found: {}", config.log_dir.display()); } @@ -75,14 +107,26 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { log::info!(" Multi FPGA: {}", config.multi_fpga); log::info!(" Waveform: {}", config.wave); log::info!(" Waveform Start Cycle: {}", config.wave_start.unwrap_or(0)); + log::info!(" Bank DiffTest: {}", config.diff); log::info!(" Trace: {:?}", config.trace); + SHOULD_EXIT.store(false, Ordering::SeqCst); + ctrlc::set_handler(|| SHOULD_EXIT.store(true, Ordering::SeqCst)) + .whatever_context("failed to set P2E Ctrl-C handler")?; + bebop_p2e::source_environment().whatever_context("failed to initialize P2E environment")?; bebop_p2e::configure_vvac_environment(); bebop_p2e::ffi::reset_runtime_state(); bebop_p2e::ffi::set_log_dir(config.log_dir.to_string_lossy().to_string()); bebop_p2e::ffi::init_cycle_trace(&config.log_dir) .map_err(|e| Whatever::without_source(format!("failed to initialize P2E cycle trace collector: {e}")))?; + #[cfg(feature = "bemu")] + let bank_digest = config.diff.then(|| { + let (bank_size, row_bytes) = bebop_bemu::private_bank_geometry(); + BankDigestConfig::new(bank_size, row_bytes) + }); + #[cfg(not(feature = "bemu"))] + let bank_digest = None; init_trace( &config.log_dir, TraceConfig { @@ -90,8 +134,8 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { mtrace: config.trace.mtrace, pmctrace: config.trace.pmctrace, ctrace: config.trace.ctrace, - banktrace: config.trace.banktrace, - bank_digest: None, + banktrace: config.trace.banktrace || config.diff, + bank_digest, }, ) .map_err(|e| Whatever::without_source(format!("failed to init P2E trace: {e}")))?; @@ -121,17 +165,57 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { let _ = std::fs::remove_file(&host_init_flag); let _ = std::fs::remove_file(&sim_exit_flag); - bebop_p2e::start_vdbg_background(&main_tcl_path).whatever_context("failed to start P2E vdbg")?; - bebop_p2e::wait_for_flash(&flash_done_flag); + let vdbg = bebop_p2e::start_vdbg_background(&main_tcl_path).whatever_context("failed to start P2E vdbg")?; + bebop_p2e::wait_for_flash(&flash_done_flag, || { + if SHOULD_EXIT.load(Ordering::SeqCst) { + return Err("P2E interrupted".to_string()); + } + Ok(()) + }) + .whatever_context("P2E flash failed")?; let _ctb = bebop_p2e::init_ctb(&case_home, &rtcfg_path).whatever_context("failed to initialize P2E CTB")?; + let _vdbg = vdbg.exit_on_drop(sim_exit_flag.clone()); + #[cfg(feature = "bemu")] + let mut diff_session = config + .diff + .then(|| { + DiffSession::new( + config.golden_elf.as_deref().expect("validated golden ELF"), + &config.log_dir, + config.golden_pk, + ) + }) + .transpose()?; + #[cfg(feature = "bemu")] + if let Some(diff) = diff_session.as_mut() { + diff.start_golden_background()?; + } std::fs::write(&host_init_flag, "").whatever_context("failed to signal P2E host init")?; - let result = bebop_p2e::wait_for_completion().whatever_context("P2E simulation failed")?; + let result = bebop_p2e::wait_for_completion(|| { + #[cfg(feature = "bemu")] + if config.diff { + poll_bank_digest()?; + } + if SHOULD_EXIT.load(Ordering::SeqCst) { + return Err("P2E interrupted".to_string()); + } + Ok(()) + }) + .map_err(|error| Whatever::without_source(format!("P2E simulation failed: {error}")))?; drop(console); bebop_p2e::ffi::finish_cycle_trace() .map_err(|e| Whatever::without_source(format!("failed to finalize P2E cycle trace: {e}")))?; + #[cfg(feature = "bemu")] + let diff_summary = if let Some(mut diff) = diff_session { + finish_bank_digest().map_err(Whatever::without_source)?; + diff.finish_golden()?; + Some(diff.finish()?) + } else { + None + }; write_trace_summary(&config.log_dir).whatever_context("failed to write P2E RTL trace summary")?; std::fs::write(&uart_log_path, &result.uart_log).whatever_context("failed to write P2E UART log")?; @@ -147,9 +231,31 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { println!("{}", result.uart_log); } + #[cfg(feature = "bemu")] + if let Some(summary) = diff_summary.as_ref() { + println!( + "Bank DiffTest M4 summary: pass={} mismatch={} missing_rtl={} unexpected_rtl={}", + summary.pass, summary.mismatch, summary.missing_rtl, summary.unexpected_rtl + ); + } + if result.exit_code != 0 { + #[cfg(feature = "bemu")] + if let Some(summary) = diff_summary.as_ref().filter(|summary| !summary.passed()) { + return Err(Whatever::without_source(format!( + "P2E exited with code {}; Bank DiffTest M4 failed: mismatch={} missing_rtl={} unexpected_rtl={}", + result.exit_code, summary.mismatch, summary.missing_rtl, summary.unexpected_rtl + ))); + } snafu::whatever!("P2E exited with code {}", result.exit_code); } + #[cfg(feature = "bemu")] + if let Some(summary) = diff_summary.filter(|summary| !summary.passed()) { + return Err(Whatever::without_source(format!( + "Bank DiffTest M4 failed: mismatch={} missing_rtl={} unexpected_rtl={}", + summary.mismatch, summary.missing_rtl, summary.unexpected_rtl + ))); + } Ok(()) } diff --git a/src/simulation/run.rs b/src/simulation/run.rs index 4cf35b9..3b8ee70 100644 --- a/src/simulation/run.rs +++ b/src/simulation/run.rs @@ -96,6 +96,9 @@ pub fn run(command: RunCommand) -> Result<(), Whatever> { multi_fpga, wave, wave_start, + diff, + golden_elf, + golden_pk, itrace, mtrace, pmctrace, @@ -111,6 +114,9 @@ pub fn run(command: RunCommand) -> Result<(), Whatever> { multi_fpga, wave, wave_start, + diff, + golden_elf, + golden_pk, trace: crate::simulation::p2e::run::P2eTraceConfig { itrace, mtrace, @@ -123,8 +129,8 @@ pub fn run(command: RunCommand) -> Result<(), Whatever> { #[cfg(not(feature = "p2e"))] { let _ = ( - image, bitstream, log_dir, multi_fpga, wave, wave_start, itrace, mtrace, pmctrace, ctrace, - banktrace, + image, bitstream, log_dir, multi_fpga, wave, wave_start, diff, golden_elf, golden_pk, itrace, + mtrace, pmctrace, ctrace, banktrace, ); crate::simulation::p2e::run::run_unavailable() } diff --git a/src/simulation/verilator/run.rs b/src/simulation/verilator/run.rs index 007c3cc..678417b 100644 --- a/src/simulation/verilator/run.rs +++ b/src/simulation/verilator/run.rs @@ -44,17 +44,11 @@ use bebop_verilator::{ exit_code, init_trace, setup_ctrlc_handler, should_exit, write_trace_summary, Simulator, TraceConfig, }; -#[cfg(all(feature = "verilator", feature = "bemu"))] -use bebop_bank_hash::{ - init_runtime_packet_channel, run_online_compare_with_summary, runtime_packet_status, - shutdown_runtime_packet_channel, BankHashCompareSummary, -}; -#[cfg(all(feature = "verilator", feature = "bemu"))] -use bebop_bemu::{BemuInstance, TraceConfig as BemuTraceConfig}; #[cfg(all(feature = "verilator", feature = "bemu"))] use bebop_verilator::{finish_bank_digest, poll_bank_digest, BankDigestConfig}; + #[cfg(all(feature = "verilator", feature = "bemu"))] -use std::thread::JoinHandle; +use crate::simulation::difftest::DiffSession; #[cfg(feature = "verilator")] use super::console::ConsoleServer; @@ -164,7 +158,7 @@ pub fn run(config: VerilatorRunConfig) -> Result<(), Whatever> { #[cfg(feature = "bemu")] let mut diff_session = config .diff - .then(|| DiffSession::new(&config.elf, &config.log_dir)) + .then(|| DiffSession::new(&config.elf, &config.log_dir, false)) .transpose()?; //===----------------------------------------------------------------------===// @@ -239,98 +233,6 @@ pub fn run(config: VerilatorRunConfig) -> Result<(), Whatever> { Ok(()) } -#[cfg(all(feature = "verilator", feature = "bemu"))] -struct DiffSession { - golden: BemuInstance, - worker: Option>>, -} - -#[cfg(all(feature = "verilator", feature = "bemu"))] -impl DiffSession { - fn new(elf: &Path, log_dir: &Path) -> Result { - let receiver = init_runtime_packet_channel(); - let output = log_dir.join("bank_diff.ndjson"); - let worker = std::thread::Builder::new() - .name("bank-diff-m4".to_string()) - .spawn(move || run_online_compare_with_summary(receiver, output).map_err(|error| error.to_string())) - .map_err(|error| { - shutdown_runtime_packet_channel(); - Whatever::without_source(format!("failed to start Bank DiffTest M4 worker: {error}")) - })?; - - let golden_result = (|| { - let golden_log_dir = log_dir.join("golden"); - let mut trace = BemuTraceConfig::new(false, false); - trace.btrace = true; - let mut golden = BemuInstance::new(&golden_log_dir, trace, false, false) - .whatever_context("failed to create BEMU Golden Model")?; - golden.load_elf(elf)?; - golden.init_hart(false)?; - Ok::<_, Whatever>(golden) - })(); - let golden = match golden_result { - Ok(golden) => golden, - Err(error) => { - shutdown_runtime_packet_channel(); - let _ = worker.join(); - return Err(error); - } - }; - - Ok(Self { - golden, - worker: Some(worker), - }) - } - - fn step_golden(&mut self) -> Result<(), Whatever> { - if !self.golden.finished() { - self.golden.step()?; - } - Ok(()) - } - - fn finish_golden(&mut self) -> Result<(), Whatever> { - while !self.golden.finished() { - self.golden.step()?; - } - let code = self.golden.exit_code().unwrap_or(0); - if code != 0 { - return Err(Whatever::without_source(format!( - "BEMU Golden Model exited with code {code}" - ))); - } - Ok(()) - } - - fn finish(mut self) -> Result { - let packet_status = runtime_packet_status(); - shutdown_runtime_packet_channel(); - let summary = self - .worker - .take() - .expect("DiffTest worker exists") - .join() - .map_err(|_| Whatever::without_source("Bank DiffTest M4 worker panicked".to_string()))? - .map_err(Whatever::without_source)?; - println!( - "Bank DiffTest runtime packets: submitted={} no_sink={} send_failed={}", - packet_status.submitted, packet_status.no_sink, packet_status.send_failed - ); - Ok(summary) - } -} - -#[cfg(all(feature = "verilator", feature = "bemu"))] -impl Drop for DiffSession { - fn drop(&mut self) { - shutdown_runtime_packet_channel(); - if let Some(worker) = self.worker.take() { - let _ = worker.join(); - } - } -} - #[cfg(not(feature = "verilator"))] pub fn run_unavailable() -> Result<(), Whatever> { Err(Whatever::without_source( From de23c0025725a20815221f48502b34c5bf74c856 Mon Sep 17 00:00:00 2001 From: daiyongyuan <1533208939@qq.com> Date: Sat, 12 Sep 2026 12:10:51 +0800 Subject: [PATCH 2/3] fix: fail when vdbg exits during flash --- src/nodes/p2e/src/runner/runworkload.rs | 33 ++++++++++++++++++++++--- src/simulation/p2e/run.rs | 4 +-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/nodes/p2e/src/runner/runworkload.rs b/src/nodes/p2e/src/runner/runworkload.rs index 5cd74bc..ee34b36 100644 --- a/src/nodes/p2e/src/runner/runworkload.rs +++ b/src/nodes/p2e/src/runner/runworkload.rs @@ -219,17 +219,30 @@ pub fn configure_vvac_environment() { log::info!("Running P2E in onboard mode"); } -pub fn wait_for_flash(flash_done_flag: &Path, mut poll: impl FnMut() -> Result<(), String>) -> Result<(), String> { - while !flash_done_flag.exists() { +pub fn wait_for_flash( + flash_done_flag: &Path, + vdbg: &mut VdbgProcess, + mut poll: impl FnMut() -> Result<(), String>, +) -> Result<(), String> { + loop { poll()?; + if flash_done_flag.exists() { + return Ok(()); + } + if let Some(status) = vdbg + .child + .try_wait() + .map_err(|error| format!("failed to query vdbg status: {error}"))? + { + return Err(format!("vdbg exited before flash completed: {status}")); + } std::thread::sleep(Duration::from_millis(100)); } - Ok(()) } #[cfg(test)] mod tests { - use super::VdbgProcess; + use super::{wait_for_flash, VdbgProcess}; use std::os::unix::process::CommandExt; use std::process::Command; @@ -260,4 +273,16 @@ mod tests { assert!(exit_flag.is_file()); std::fs::remove_file(exit_flag).unwrap(); } + + #[test] + fn flash_wait_fails_when_vdbg_exits() { + let flash_done = std::env::temp_dir().join(format!("bebop-p2e-flash-{}", std::process::id())); + let _ = std::fs::remove_file(&flash_done); + let child = Command::new("false").spawn().unwrap(); + let mut vdbg = VdbgProcess { child, exit_flag: None }; + + let error = wait_for_flash(&flash_done, &mut vdbg, || Ok(())).unwrap_err(); + + assert!(error.starts_with("vdbg exited before flash completed:")); + } } diff --git a/src/simulation/p2e/run.rs b/src/simulation/p2e/run.rs index 085f400..151b177 100644 --- a/src/simulation/p2e/run.rs +++ b/src/simulation/p2e/run.rs @@ -165,8 +165,8 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { let _ = std::fs::remove_file(&host_init_flag); let _ = std::fs::remove_file(&sim_exit_flag); - let vdbg = bebop_p2e::start_vdbg_background(&main_tcl_path).whatever_context("failed to start P2E vdbg")?; - bebop_p2e::wait_for_flash(&flash_done_flag, || { + let mut vdbg = bebop_p2e::start_vdbg_background(&main_tcl_path).whatever_context("failed to start P2E vdbg")?; + bebop_p2e::wait_for_flash(&flash_done_flag, &mut vdbg, || { if SHOULD_EXIT.load(Ordering::SeqCst) { return Err("P2E interrupted".to_string()); } From 391dd845590afa66774b642d72f295522cb342d9 Mon Sep 17 00:00:00 2001 From: daiyongyuan <1533208939@qq.com> Date: Sat, 12 Sep 2026 18:22:25 +0800 Subject: [PATCH 3/3] feat: complete P2E bank difftest support --- src/nodes/bemu/src/emu/inst/32_mset.rs | 55 ++++++++++++++++++++- src/nodes/p2e/src/ctb/ffi.rs | 67 +++++++++++++++++++++----- src/simulation/p2e/run.rs | 67 ++++++++++++++++++++++++-- 3 files changed, 172 insertions(+), 17 deletions(-) diff --git a/src/nodes/bemu/src/emu/inst/32_mset.rs b/src/nodes/bemu/src/emu/inst/32_mset.rs index d1ef088..e3211cc 100644 --- a/src/nodes/bemu/src/emu/inst/32_mset.rs +++ b/src/nodes/bemu/src/emu/inst/32_mset.rs @@ -14,8 +14,21 @@ impl Instruction for Mset { let (_rows, col, alloc, clear) = xs2_mset(xs2); let v = bank_id as u32; - let groups = col.max(1); let shared_bank = is_shared_vbank(bank_id); + let groups = if alloc == 1 && col == 0 { + if shared_bank { + ctx.shared + .as_ref() + .expect("shared bank storage is unavailable") + .bank_map + .slots + .len() as u64 + } else { + crate::config::bank_num() as u64 + } + } else { + col.max(1) + }; if alloc == 1 { let mut allocated = Vec::with_capacity(groups as usize); @@ -50,7 +63,7 @@ impl Instruction for Mset { } *ctx.config_mut(bank_id) = BankConfig { allocated: true, - cols: col, + cols: groups, valid_rows: 0, }; } else { @@ -112,4 +125,42 @@ mod tests { assert_eq!(context.bank_map.resolve(1), Some(1)); assert_eq!(context.banks[1][0], 0); } + + #[test] + fn zero_column_allocation_uses_all_banks_and_reallocates() { + crate::config::configure_default(); + let mut memory = Vec::new(); + let mut storage = (0..bank_num()) + .map(|_| PrivateBank::new(bank_size())) + .collect::>(); + let mut configs = vec![BankConfig::default(); bank_num()]; + let mut bank_map = BankMap::new(bank_num()); + let mut deferred = Vec::new(); + let mut mmio = Vec::new(); + let mut barrier = false; + let mut context = ExecContext { + hart_id: 0, + instruction_id: 0, + memory: &mut memory, + banks: TrackedBanks::new(&mut storage, None, 0), + cfgs: &mut configs, + bank_map: &mut bank_map, + shared: None, + deferred_bank_frees: &mut deferred, + mmio_banks: &mut mmio, + barrier_hit: &mut barrier, + }; + + let alloc_all = 0x401; + Mset::exec(0, alloc_all, &mut context); + assert_eq!(context.config(0).cols, bank_num() as u64); + for group in 0..bank_num() { + assert_eq!(context.bank_map.resolve_group(0, group as u32), Some(group)); + } + + Mset::exec(0, alloc_all, &mut context); + for group in 0..bank_num() { + assert_eq!(context.bank_map.resolve_group(0, group as u32), Some(group)); + } + } } diff --git a/src/nodes/p2e/src/ctb/ffi.rs b/src/nodes/p2e/src/ctb/ffi.rs index c51495f..946de32 100644 --- a/src/nodes/p2e/src/ctb/ffi.rs +++ b/src/nodes/p2e/src/ctb/ffi.rs @@ -7,6 +7,7 @@ use std::io::Write; use std::os::unix::fs::FileExt; use std::sync::mpsc::Sender; use std::sync::{Mutex, OnceLock}; +use std::time::Instant; const SIM_EXIT_ADDR: u64 = 0x6000_0000; const UART_BASE_ADDR: u64 = 0x6002_0000; @@ -58,6 +59,9 @@ struct RuntimeState { console_tx: Option>, cycle_trace: Option, cycle_trace_error: Option, + uart_line: Vec, + executable_started: Option<(String, Instant)>, + executable_timing: Option<(String, Instant, Instant)>, } static STATE: OnceLock> = OnceLock::new(); @@ -136,6 +140,39 @@ pub fn uart_log() -> String { String::from_utf8_lossy(&guard.uart_log).to_string() } +pub fn executable_timing() -> Option<(String, Instant, Instant)> { + state().lock().unwrap().executable_timing.clone() +} + +fn record_uart_byte(guard: &mut RuntimeState, hart_id: u32, byte: u8) { + guard.uart_log.push(byte); + if let Some(collector) = guard.cycle_trace.as_mut() { + if let Err(error) = collector.push_uart_byte(hart_id, byte) { + guard.cycle_trace_error.get_or_insert(error); + } + } + if hart_id != 0 { + return; + } + guard.uart_line.push(byte); + if byte != b'\n' { + return; + } + + let bytes = std::mem::take(&mut guard.uart_line); + let line = String::from_utf8_lossy(&bytes).trim().to_string(); + if let Some(name) = line.strip_prefix("RUN ") { + guard.executable_started = Some((name.to_string(), Instant::now())); + } + if let Some(name) = line.strip_prefix("PASS ") { + if let Some((running, started)) = guard.executable_started.take() { + if running == name { + guard.executable_timing = Some((running, started, Instant::now())); + } + } + } +} + pub fn host_mmio_write(addr: u64, data: u64) -> i32 { let mut guard = state().lock().unwrap(); @@ -152,12 +189,7 @@ pub fn host_mmio_write(addr: u64, data: u64) -> i32 { if (UART_BASE_ADDR..UART_BASE_ADDR + UART_SIZE).contains(&addr) { if addr == UART_BASE_ADDR { let byte = (data & 0xff) as u8; - guard.uart_log.push(byte); - if let Some(collector) = guard.cycle_trace.as_mut() { - if let Err(error) = collector.push_uart_byte(0, byte) { - guard.cycle_trace_error.get_or_insert(error); - } - } + record_uart_byte(&mut guard, 0, byte); print!("{}", byte as char); let _ = std::io::Write::flush(&mut std::io::stdout()); } @@ -202,12 +234,7 @@ pub extern "C" fn scu_uart_write(hart_id: u32, ch: u32) { // Also write to global uart_log for backward compatibility let byte = (ch & 0xff) as u8; - guard.uart_log.push(byte); - if let Some(collector) = guard.cycle_trace.as_mut() { - if let Err(error) = collector.push_uart_byte(hart_id, byte) { - guard.cycle_trace_error.get_or_insert(error); - } - } + record_uart_byte(&mut guard, hart_id, byte); if let Some(tx) = &guard.console_tx { let _ = tx.send(UartTx { hart_id, byte }); } @@ -483,3 +510,19 @@ impl Drop for CtbManager { self.quit(); } } + +#[cfg(test)] +mod tests { + use super::{record_uart_byte, RuntimeState}; + + #[test] + fn times_linux_executable_between_run_and_pass() { + let mut state = RuntimeState::default(); + for byte in b"RUN buddy-buckyball-lenet-run\nPASS buddy-buckyball-lenet-run\n" { + record_uart_byte(&mut state, 0, *byte); + } + + let (name, _, _) = state.executable_timing.unwrap(); + assert_eq!(name, "buddy-buckyball-lenet-run"); + } +} diff --git a/src/simulation/p2e/run.rs b/src/simulation/p2e/run.rs index 151b177..9503058 100644 --- a/src/simulation/p2e/run.rs +++ b/src/simulation/p2e/run.rs @@ -5,6 +5,8 @@ use snafu::Whatever; use std::path::PathBuf; #[cfg(feature = "p2e")] use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "p2e")] +use std::time::Instant; #[cfg(feature = "p2e")] use bebop_p2e::{self}; @@ -53,6 +55,7 @@ pub struct P2eTraceConfig { #[cfg(feature = "p2e")] pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).try_init(); + let command_started = Instant::now(); if config.diff && config.golden_elf.is_none() { return Err(Whatever::without_source( @@ -177,6 +180,8 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { let _ctb = bebop_p2e::init_ctb(&case_home, &rtcfg_path).whatever_context("failed to initialize P2E CTB")?; let _vdbg = vdbg.exit_on_drop(sim_exit_flag.clone()); #[cfg(feature = "bemu")] + let diff_started = config.diff.then(Instant::now); + #[cfg(feature = "bemu")] let mut diff_session = config .diff .then(|| { @@ -204,18 +209,25 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { Ok(()) }) .map_err(|error| Whatever::without_source(format!("P2E simulation failed: {error}")))?; + let executable_timing = bebop_p2e::ffi::executable_timing(); drop(console); bebop_p2e::ffi::finish_cycle_trace() .map_err(|e| Whatever::without_source(format!("failed to finalize P2E cycle trace: {e}")))?; + let simulation_finished = Instant::now(); #[cfg(feature = "bemu")] - let diff_summary = if let Some(mut diff) = diff_session { + let (diff_summary, diff_elapsed) = if let Some(mut diff) = diff_session { finish_bank_digest().map_err(Whatever::without_source)?; diff.finish_golden()?; - Some(diff.finish()?) + let summary = diff.finish()?; + ( + Some(summary), + Some(diff_started.expect("diff session start exists").elapsed()), + ) } else { - None + (None, None) }; + let diff_finished = Instant::now(); write_trace_summary(&config.log_dir).whatever_context("failed to write P2E RTL trace summary")?; std::fs::write(&uart_log_path, &result.uart_log).whatever_context("failed to write P2E UART log")?; @@ -239,6 +251,55 @@ pub fn run(config: P2eRunConfig) -> Result<(), Whatever> { ); } + let command_finished = Instant::now(); + let mut timing_summary = String::from("P2E timing summary (host wall-clock, additive phases):\n"); + if let Some((name, executable_started, executable_finished)) = executable_timing { + let before_executable = executable_started.duration_since(command_started); + let executable_elapsed = executable_finished.duration_since(executable_started); + let after_executable = simulation_finished.duration_since(executable_finished); + let diff_finalize = diff_finished.duration_since(simulation_finished); + let output_cleanup = command_finished.duration_since(diff_finished); + timing_summary.push_str(&format!( + " P2E setup/Linux boot: {:.3} s\n Linux executable {name}: {:.3} s\n FPGA completion: {:.3} s\n", + before_executable.as_secs_f64(), + executable_elapsed.as_secs_f64(), + after_executable.as_secs_f64() + )); + #[cfg(feature = "bemu")] + if config.diff { + timing_summary.push_str(&format!( + " Bank DiffTest finalization: {:.3} s\n", + diff_finalize.as_secs_f64() + )); + } else { + timing_summary.push_str(&format!(" Trace finalization: {:.3} s\n", diff_finalize.as_secs_f64())); + } + #[cfg(not(feature = "bemu"))] + timing_summary.push_str(&format!(" Trace finalization: {:.3} s\n", diff_finalize.as_secs_f64())); + timing_summary.push_str(&format!( + " Output cleanup: {:.3} s\n Total P2E command: {:.3} s\n", + output_cleanup.as_secs_f64(), + command_finished.duration_since(command_started).as_secs_f64() + )); + } else { + timing_summary.push_str(&format!( + " Linux executable timing unavailable: RUN/PASS markers not observed\n FPGA workload: {:.3} s\n Total P2E command: {:.3} s\n", + result.elapsed.as_secs_f64(), + command_finished.duration_since(command_started).as_secs_f64() + )); + } + #[cfg(feature = "bemu")] + if let Some(elapsed) = diff_elapsed { + timing_summary.push_str(&format!( + " Bank DiffTest full session (parallel reference, excluded from total): {:.3} s\n", + elapsed.as_secs_f64() + )); + } + print!("{timing_summary}"); + let timing_log_path = config.log_dir.join("p2e_timing.log"); + std::fs::write(&timing_log_path, timing_summary).whatever_context("failed to write P2E timing log")?; + println!(" Timing log: {}", timing_log_path.display()); + if result.exit_code != 0 { #[cfg(feature = "bemu")] if let Some(summary) = diff_summary.as_ref().filter(|summary| !summary.passed()) {