Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/aig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,86 @@ impl AIG {

path
}

/// Dump detailed critical paths with cell origins, delays, and cumulative arrivals.
///
/// Returns a formatted string showing:
/// - For each critical endpoint, the full path from source to sink
/// - Each node's cell origin (synthesis cell that created it)
/// - Gate delays from the Liberty library
/// - Cumulative arrival times
pub fn dump_critical_paths_detailed(&self, limit: usize) -> String {
let mut output = String::new();
output.push_str("=== AIG Critical Path Analysis ===\n\n");

let critical_paths = self.get_critical_paths(limit);

for (endpoint_idx, (endpoint, arrival)) in critical_paths.iter().enumerate() {
let slack = self.clock_period_ps as i64 - *arrival as i64;

// Determine endpoint label
let endpoint_label = if self.primary_outputs.contains(endpoint) {
format!("Primary Output (aigpin {})", endpoint)
} else {
// Find which DFF this belongs to
let mut dff_label = format!("DFF D-input (aigpin {})", endpoint);
for (_cell_id, dff) in &self.dffs {
if (dff.d_iv >> 1) == *endpoint {
dff_label = format!("DFF D-input (aigpin {}) [unknown DFF]", endpoint);
break;
}
}
dff_label
};

output.push_str(&format!(
"#{}: {} arrival={} ps, slack={} ps\n",
endpoint_idx + 1,
endpoint_label,
arrival,
slack
));

// Trace the path back
let path = self.trace_critical_path(*endpoint);

output.push_str("Path (source → sink):\n");
for (depth, (node, node_arrival)) in path.iter().enumerate() {
let (rise_delay, fall_delay) = self.gate_delays[*node];
let driver_label = match &self.drivers[*node] {
DriverType::AndGate(_, _) => "AND".to_string(),
DriverType::InputPort(_) => "INPUT".to_string(),
DriverType::InputClockFlag(_, _) => "CLOCK".to_string(),
DriverType::DFF(_) => "DFF_Q".to_string(),
DriverType::SRAM(_) => "SRAM_READ".to_string(),
DriverType::Tie0 => "TIE0".to_string(),
};

let max_delay = rise_delay.max(fall_delay);
output.push_str(&format!(
" [{:2}] aigpin {:5} | type: {:10} | delay: {:5} ps | arrival: {:5} ps",
depth, node, driver_label, max_delay, node_arrival
));

// Show cell origins if available
if !self.aigpin_cell_origins[*node].is_empty() {
let origins = &self.aigpin_cell_origins[*node];
for (cell_id, cell_type, pin_name) in origins {
output.push_str(&format!(
" | cell: {} {} (id:{})",
cell_type, pin_name, cell_id
));
}
} else if *node > 0 {
output.push_str(" | (internal)");
}
output.push('\n');
}
output.push('\n');
}

output
}
}

/// A reusable topological traverser with dense visited buffer.
Expand Down
101 changes: 99 additions & 2 deletions src/bin/jacquard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use jacquard::aig::AIG;
use jacquard::sim::setup::DesignArgs;

#[derive(Parser)]
Expand All @@ -31,6 +30,12 @@ enum Commands {
/// a cycle-accurate co-simulation with GPU-side SPI flash and UART models.
/// Requires building with `--features metal`.
Cosim(CosimArgs),

/// Dump AIG critical paths with timing details.
///
/// Analyzes the AIG timing and shows the critical paths from source to sink,
/// including cell origins (synthesis cells), gate delays, and cumulative arrivals.
DumpPaths(DumpPathsArgs),
}

#[derive(Parser)]
Expand Down Expand Up @@ -188,6 +193,49 @@ struct CosimArgs {
/// by their computed arrival times. Forces single-tick mode.
#[clap(long)]
timing_vcd: Option<PathBuf>,

/// Dump all DFF Q-values per cycle to a text file for debugging.
/// Forces single-tick mode for the specified number of cycles (default 20).
#[clap(long)]
dump_dff: Option<PathBuf>,

/// Number of cycles to dump DFF states for (used with --dump-dff).
#[clap(long, default_value = "20")]
dump_dff_cycles: usize,
}

#[derive(Parser)]
struct DumpPathsArgs {
/// Gate-level Verilog path synthesized with AIGPDK or SKY130 library.
netlist_verilog: PathBuf,

/// Top module name in the netlist.
#[clap(long)]
top_module: Option<String>,

/// Level split thresholds (must match values used during mapping).
#[clap(long, value_delimiter = ',')]
level_split: Vec<usize>,

/// Path to Liberty library file for timing data.
#[clap(long)]
liberty: Option<PathBuf>,

/// Clock period in picoseconds for timing analysis.
#[clap(long, default_value = "1000")]
clock_period: u64,

/// Path to SDF file for per-instance back-annotated delays.
#[clap(long)]
sdf: Option<PathBuf>,

/// SDF corner selection: min, typ, or max.
#[clap(long, default_value = "typ")]
sdf_corner: String,

/// Number of critical paths to dump (default: 5).
#[clap(long, default_value = "5")]
limit: usize,
}

#[allow(unused_variables)]
Expand Down Expand Up @@ -1150,7 +1198,7 @@ fn sim_hip(
}

#[cfg(any(feature = "metal", feature = "cuda", feature = "hip"))]
fn run_timing_analysis(aig: &mut AIG, args: &SimArgs) {
fn run_timing_analysis(aig: &mut jacquard::aig::AIG, args: &SimArgs) {
use jacquard::liberty_parser::TimingLibrary;

clilog::info!("Running timing analysis on GPU simulation results...");
Expand Down Expand Up @@ -1224,6 +1272,52 @@ fn run_timing_analysis(aig: &mut AIG, args: &SimArgs) {
clilog::finish!(timer_timing);
}

fn cmd_dump_paths(args: DumpPathsArgs) {
use jacquard::liberty_parser::TimingLibrary;
use jacquard::sim::setup;

clilog::info!("Loading design for critical path analysis...");
let timer = clilog::stimer!("load_design");

let design_args = DesignArgs {
netlist_verilog: args.netlist_verilog.clone(),
top_module: args.top_module.clone(),
level_split: args.level_split.clone(),
num_blocks: 1, // Not needed for path analysis
json_path: None,
sdf: args.sdf.clone(),
sdf_corner: args.sdf_corner.clone(),
sdf_debug: false,
clock_period_ps: Some(args.clock_period),
xprop: false,
liberty: args.liberty.clone(),
};

let mut design = setup::load_design(&design_args);
clilog::finish!(timer);

clilog::info!("Loading timing library...");
let lib = if let Some(lib_path) = &args.liberty {
TimingLibrary::from_file(lib_path).expect("Failed to load Liberty library")
} else {
TimingLibrary::load_aigpdk().expect("Failed to load default AIGPDK library")
};
clilog::info!("Loaded Liberty library: {}", lib.name);

let aig = &mut design.aig;
aig.load_timing_library(&lib);
aig.clock_period_ps = args.clock_period;

clilog::info!("Computing timing analysis...");
let timer_timing = clilog::stimer!("compute_timing");
let _report = aig.compute_timing();
clilog::finish!(timer_timing);

// Dump critical paths
let output = aig.dump_critical_paths_detailed(args.limit);
println!("{}", output);
}

fn main() {
clilog::init_stderr_color_debug();
clilog::set_max_print_count(clilog::Level::Warn, "NL_SV_LIT", 1);
Expand All @@ -1232,6 +1326,7 @@ fn main() {
match cli.command {
Commands::Sim(args) => cmd_sim(args),
Commands::Cosim(args) => cmd_cosim(args),
Commands::DumpPaths(args) => cmd_dump_paths(args),
}
}

Expand Down Expand Up @@ -1317,6 +1412,8 @@ fn cmd_cosim(args: CosimArgs) {
clock_period: args.clock_period,
stimulus_vcd: args.stimulus_vcd.clone(),
timing_vcd: args.timing_vcd.clone(),
dump_dff: args.dump_dff.clone(),
dump_dff_cycles: args.dump_dff_cycles,
};

let result =
Expand Down
45 changes: 45 additions & 0 deletions src/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,51 @@ impl FlattenedScriptV1 {
cellid_to_sdf_path.insert(cellid, sdf_path);
}

// Detect hierarchy prefix mismatch: if the netlist has a wrapper hierarchy
// (e.g., openframe_project_wrapper → top top_inst → cells), the NetlistDB
// paths will be "top_inst._58619_" but the SDF (generated against the flat
// `top` module) will have just "_58619_".
// Fix: find a common prefix in NetlistDB paths that doesn't exist in SDF,
// and strip it.
{
// Sample a few paths and check if they match SDF cells
let mut sample_hits = 0usize;
let mut sample_misses = 0usize;
let mut common_prefix: Option<String> = None;
for (_, path) in cellid_to_sdf_path.iter().take(100) {
if sdf.get_cell(path).is_some() {
sample_hits += 1;
} else {
sample_misses += 1;
// Check if stripping the first dot-separated component helps
if let Some(dot_pos) = path.find('.') {
let stripped = &path[dot_pos + 1..];
if sdf.get_cell(stripped).is_some() {
let prefix = &path[..dot_pos];
if common_prefix.is_none() {
common_prefix = Some(prefix.to_string());
}
}
}
}
}
// If most lookups fail but stripping a prefix works, apply globally
if sample_misses > sample_hits && common_prefix.is_some() {
let prefix = common_prefix.unwrap();
let prefix_dot = format!("{}.", prefix);
clilog::info!(
"SDF hierarchy prefix mismatch detected: stripping '{}' from {} cell paths",
prefix_dot,
cellid_to_sdf_path.len()
);
for path in cellid_to_sdf_path.values_mut() {
if let Some(stripped) = path.strip_prefix(&prefix_dot) {
*path = stripped.to_string();
}
}
}
}

// Build reverse map: SDF path → cellid for efficient lookup
let mut sdf_path_to_cellid: HashMap<&str, usize> = HashMap::new();
for (&cellid, path) in &cellid_to_sdf_path {
Expand Down
Loading
Loading