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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,29 @@ jobs:
1
python3 tests/xprop_cosim/check.py target/test-out/ci_sim_2state.vcd two-state

# Register value-injection regression (#108). A/B on the same SRAM-less
# design: cosim --xprop with NO reg_init leaves the unreset counter X
# (q_unreset stays x); cosim --xprop WITH a reg_init $deposit on
# `unreset_count` clears the power-up X so q_unreset reads known 0/1.
# This is the maintainer's #102 fix-pattern in miniature (deposit on an
# unreset register → downstream definite) and the first cosim run of a
# zero-SRAM design (guards the nil-MTLBuffer fix).
- name: Register value-injection demo — cosim (#108)
run: |
set -e
cargo run --release --features metal --bin jacquard -- cosim \
tests/xprop_cosim/xprop_demo_synth.gv \
--config tests/xprop_cosim/sim_config.json \
--output-vcd target/test-out/ci_cosim_noreginit.vcd \
--max-clock-edges 200 --xprop
python3 tests/xprop_cosim/check.py target/test-out/ci_cosim_noreginit.vcd xprop
cargo run --release --features metal --bin jacquard -- cosim \
tests/xprop_cosim/xprop_demo_synth.gv \
--config tests/xprop_cosim/sim_config_reginit.json \
--output-vcd target/test-out/ci_cosim_reginit.vcd \
--max-clock-edges 200 --xprop
python3 tests/xprop_cosim/check.py target/test-out/ci_cosim_reginit.vcd reg-init

- name: Run Metal simulation with timing (inv_chain_pnr)
run: |
set -o pipefail
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ the public contracts in `docs/release-process.md` follow stricter rules).

## [Unreleased]

### Added

- **`reg_init` register value-injection for cosim** (#108). A new
`reg_init` array in the testbench JSON deposits a definite value into
chosen registers at tick 0 with `$deposit` semantics — the seed clears
the power-up X-mask, then the design's own logic drives the register
normally (NOT `force`, which would pin a CDC crossing register and write
zeros across the handshake). Each entry is `{ "name", "value", "width" }`;
a multi-bit register resolves `name[0]`..`name[width-1]` independently.
This is the register sibling of `sram_init` and the fix path for
X-poisoned unreset CDC launch registers (#102): depositing on the launch
flops lets X-aware cosim of debug-loaded firmware proceed. Composes with
the x-assert detection work (#106). Regression: `tests/xprop_cosim/`
`reg-init` mode (cosim A/B — deposit clears the X that `xprop` mode
requires to persist).

### Fixed

- **cosim on SRAM-less designs** no longer panics. A design with zero SRAM
produced a nil `MTLBuffer` (`new_buffer(0)`) whose `.contents()` is null;
the SRAM data buffer is now sized to `max(1)` word (matching the X-mask
shadow buffer), so pure-logic designs run under `jacquard cosim`.

## [0.1.0] - 2026-06-04

First numbered release. Metal (macOS/Apple Silicon) is the shipped
Expand Down
66 changes: 63 additions & 3 deletions src/sim/cosim_metal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,6 @@ fn set_bit(state: &mut [u32], pos: u32, val: u8) {
}

/// Clear a single bit in a packed u32 state buffer.
#[allow(dead_code)]
#[inline]
fn clear_bit(state: &mut [u32], pos: u32) {
state[(pos >> 5) as usize] &= !(1u32 << (pos & 31));
Expand Down Expand Up @@ -2589,9 +2588,14 @@ pub fn run_cosim(
);
}

// SRAM storage
// SRAM storage. Allocate at least one word: a SRAM-less design has
// `sram_storage_size == 0`, and `new_buffer(0)` returns a nil MTLBuffer
// whose `.contents()` is null (foreign-types asserts non-null). Sizing to
// `max(1)` while slicing to the real length keeps SRAM-free designs (e.g.
// pure-logic cosim) working; the kernel guards SRAM reads on the cell map.
let sram_data_len = (script.sram_storage_size as usize).max(1);
let sram_data_buffer = simulator.device.new_buffer(
(script.sram_storage_size as usize * std::mem::size_of::<u32>()) as u64,
(sram_data_len * std::mem::size_of::<u32>()) as u64,
MTLResourceOptions::StorageModeShared,
);
let sram_data: &mut [u32] = unsafe {
Expand Down Expand Up @@ -2675,6 +2679,62 @@ pub fn run_cosim(
}
let _ = sram_data;

// Register value-injection (issue #108, ADR 0016). `reg_init` deposits a
// definite value into chosen registers at tick 0 with `$deposit`
// semantics — the seed clears the power-up X-mask, then the design drives
// the register normally. The register sibling of `sram_init` above; the
// fix path for X-poisoned unreset CDC launch registers (#102). NOT
// `force`: applied once here, not re-driven each edge.
//
// A DFF Q is sequential state, not a primary input in the per-edge BitOp
// schedule, so a deposit must survive `state_prep`'s output→input copy
// (kernel_v1.metal): write the value AND clear the X-mask in BOTH slots,
// mirroring the xprop seed above. The output slot is the source of truth
// the first `simulate` reads; the input slot covers any pre-copy read.
if !config.reg_init.is_empty() {
let rio = script.reg_io_state_size as usize;
let mut deposited_bits = 0usize;
for entry in &config.reg_init {
let width = entry.width.unwrap_or(1);
for b in 0..width {
// A scalar register (width 1, however spelled) resolves by its
// bare name; a wider one resolves each bit `name[b]` (a
// register's bits need not be contiguous after synthesis).
let bit_name = if width == 1 {
entry.name.clone()
} else {
format!("{}[{}]", entry.name, b)
};
let pos = crate::sim::trace_signals::resolve_to_input_state_pos(
aig, netlistdb, script, &bit_name,
)
.unwrap_or_else(|| {
panic!(
"reg_init: cannot resolve register '{}' to a DFF state slot \
(not a register, or name not found in netlist)",
bit_name
)
});
let val = if b < 64 { ((entry.value >> b) & 1) as u8 } else { 0 };
// Deposit the value into both slots' value sections.
set_bit(&mut states[..state_size], pos, val);
set_bit(&mut states[state_size..2 * state_size], pos, val);
// Clear the X-mask in both slots so the deposit reads known.
// The X-mask section starts `rio` words into each slot.
if script.xprop_enabled {
clear_bit(&mut states[rio..state_size], pos);
clear_bit(&mut states[state_size + rio..2 * state_size], pos);
}
deposited_bits += 1;
}
}
clilog::info!(
"reg_init: deposited {} register bit(s) from {} config entries (cleared power-up X)",
deposited_bits,
config.reg_init.len()
);
}

// Initialize: set reset active
let reset_val = if config.reset_active_high { 1u8 } else { 0u8 };
set_bit(
Expand Down
53 changes: 48 additions & 5 deletions src/sim/trace_signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,17 +301,25 @@ pub fn register_trace_signals(
///
/// Used by the cosim bus-trace param builder to bind each protocol pin
/// to a state position the GPU kernel can read.
/// Parse `name` and resolve it to a netlistdb net id, scanning the
/// multi-candidate interpretations (`parse_signal_name`) until one hits
/// `netname2id`. Shared by the state-position resolvers below. Returns `None`
/// if the name can't be parsed or matches no net.
fn resolve_net_id(netlistdb: &NetlistDB, name: &str) -> Option<usize> {
let candidates = parse_signal_name(name).ok()?;
candidates.iter().find_map(|c| {
let key = (c.hier.clone(), c.leaf.clone(), c.bit);
netlistdb.netname2id.get(&key).copied()
})
}

pub fn resolve_to_state_pos(
aig: &AIG,
netlistdb: &NetlistDB,
script: &FlattenedScriptV1,
name: &str,
) -> Option<u32> {
let candidates = parse_signal_name(name).ok()?;
let netid = candidates.iter().find_map(|c| {
let key = (c.hier.clone(), c.leaf.clone(), c.bit);
netlistdb.netname2id.get(&key).copied()
})?;
let netid = resolve_net_id(netlistdb, name)?;
let pinid = netlistdb.net2pin.iter_set(netid).next()?;
let iv = aig.pin2aigpin_iv[pinid];
if iv == usize::MAX {
Expand All @@ -328,6 +336,41 @@ pub fn resolve_to_state_pos(
None
}

/// Resolve a hierarchical register (DFF) name to its bit position in the
/// **input** state slot — the DFF Q slot a power-up value must be deposited
/// into for `reg_init` value-injection (issue #108).
///
/// Distinct from [`resolve_to_state_pos`], which reads `output_map` (keyed by
/// DFF *D* inputs) for bus-trace pin binding. A register *name* resolves to
/// the net driven by the DFF's *Q*, whose aigpin lives in `input_map`.
/// Returns `None` if the name doesn't resolve to a DFF Q present in the
/// flattened script (a pure-comb net, a constant, or an unknown name).
pub fn resolve_to_input_state_pos(
aig: &AIG,
netlistdb: &NetlistDB,
script: &FlattenedScriptV1,
name: &str,
) -> Option<u32> {
let netid = resolve_net_id(netlistdb, name)?;
// Scan every pin on the net, not just the first: the DFF Q *driver* pin
// is the one keyed in `input_map`, but it need not be the net's first pin
// — load pins (e.g. adder inputs consuming the register) share the net.
// `input_map` is keyed by the **bare** aigpin (`dff.q`, from
// `add_aigpin`), whereas `pin2aigpin_iv` is iv-encoded
// (`aigpin << 1 | invert`); recover the bare aigpin with `>> 1`, which
// also drops the per-pin invert so both polarities collapse to one key.
for pinid in netlistdb.net2pin.iter_set(netid) {
let iv = aig.pin2aigpin_iv[pinid];
if iv == usize::MAX {
continue;
}
if let Some(&pos) = script.input_map.get(&(iv >> 1)) {
return Some(pos);
}
}
None
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
33 changes: 33 additions & 0 deletions src/testbench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ pub struct TestbenchConfig {
#[serde(default)]
pub jtag: Option<JtagConfig>,
pub sram_init: Option<SramInitConfig>,
/// Register value-injection (`$deposit` at tick 0). See [`RegInitEntry`]
/// and issue #108. The register sibling of `sram_init`: clears power-up X
/// on chosen registers (e.g. unreset CDC launch flops) so X-aware cosim of
/// debug-loaded firmware can proceed (#102). Empty when absent.
#[serde(default)]
pub reg_init: Vec<RegInitEntry>,
pub output_events: Option<String>,
pub events_reference: Option<String>,
/// Path to an `input.json` file containing a list of stimulus
Expand Down Expand Up @@ -427,6 +433,33 @@ pub struct SramInitConfig {
pub elf_path: String,
}

/// One register value-injection entry for cosim (`reg_init` in the testbench
/// JSON). Deposits a definite value into a register at tick 0 with `$deposit`
/// semantics — the seed clears the power-up X-mask, then the design's own
/// logic drives the register normally. The register sibling of `sram_init`
/// (issue #108) and the fix path for X-poisoned unreset CDC launch registers
/// (#102).
///
/// NOT `force`: the value is applied once at t0, not re-driven every edge, so
/// a CDC crossing register carries real protocol data after t0 rather than
/// being pinned to the seed (which would write zeros across the handshake).
#[derive(Debug, Clone, Deserialize)]
pub struct RegInitEntry {
/// Hierarchical register (DFF Q) name. For a scalar register, the bare
/// name; for a multi-bit register, the bus base — each bit `name[b]` for
/// `b` in `0..width` is resolved independently (synthesis need not keep a
/// register's bits at contiguous state positions).
pub name: String,
/// Value deposited LSB-first across the register's bits. Defaults to 0,
/// which alone suffices to clear power-up X (the protocol then drives real
/// values). Bits at index ≥ 64 are treated as 0.
#[serde(default)]
pub value: u64,
/// Register bit width. Omit (or 1) for a scalar resolved by the bare name;
/// `> 1` resolves `name[0]`..`name[width-1]`.
pub width: Option<u32>,
}

// ── UART TX decoder ─────────────────────────────────────────────────────────

/// UART TX decoder state machine.
Expand Down
25 changes: 23 additions & 2 deletions tests/xprop_cosim/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@
that samples the undriven input). sim leaves these known
because sim inputs all come from the VCD.
two-state a run WITHOUT --xprop: no X/Z on any present signal.
reg-init cosim with --xprop AND a `reg_init` deposit on the unreset
counter (issue #108): q_unreset must now be KNOWN 0/1 for the
whole run — the $deposit cleared the power-up X that mode
`xprop` requires to persist. The inverse assertion of `xprop`,
proving register value-injection works end to end.

Usage:
check.py <output.vcd> {xprop|xprop-cosim|two-state}
check.py <output.vcd> {xprop|xprop-cosim|two-state|reg-init}
"""
import sys

Expand All @@ -41,7 +46,7 @@
# Undriven-input outputs, only meaningful in cosim (#95 phase 3).
UNDRIVEN = ("q_undriven_comb", "q_undriven_reg")
SIGNALS = CORE + UNDRIVEN
MODES = ("xprop", "xprop-cosim", "two-state")
MODES = ("xprop", "xprop-cosim", "two-state", "reg-init")


def parse_vcd(path):
Expand Down Expand Up @@ -120,6 +125,22 @@ def fail(msg):
bad = sig[name]["values"] & {"x", "z"}
if bad:
fail(f"{name} has {sorted(bad)} in a two-state run (no --xprop)")
elif mode == "reg-init":
# q_unreset: the unreset counter was seeded via `reg_init` ($deposit),
# so its power-up X is cleared — it must be a KNOWN 0/1 for the whole
# run (the LSB toggles each cycle from a definite seed). This is the
# exact inverse of the `xprop` mode and proves value-injection works.
bad = sig["q_unreset"]["values"] & {"x", "z"}
if bad:
fail(
f"q_unreset still has {sorted(bad)} after a reg_init deposit "
"(value-injection did not clear the power-up X)"
)
if sig["q_unreset"]["last"] not in ("0", "1"):
fail(f"q_unreset should end known, last={sig['q_unreset']['last']}")
# q_reset is unaffected by reg_init; it must still resolve.
if sig["q_reset"]["last"] not in ("0", "1"):
fail(f"q_reset should resolve to known 0/1, last={sig['q_reset']['last']}")
else: # xprop or xprop-cosim
# q_unreset: uninitialised counter, X+1=X, must be X for the whole run.
if sig["q_unreset"]["values"] != {"x"}:
Expand Down
23 changes: 23 additions & 0 deletions tests/xprop_cosim/sim_config_reginit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"_comment": "reg_init value-injection demo (#108). Same design/ports as sim_config.json, plus a reg_init that deposits a definite value into the otherwise-unreset `unreset_count` counter at tick 0 ($deposit semantics). Under --xprop this clears the power-up X, so q_unreset (= unreset_count[0]) now reads KNOWN 0/1 for the whole run instead of X. See tests/xprop_cosim/check.py mode reg-init.",
"netlist_path": "tests/xprop_cosim/xprop_demo_synth.gv",
"clock_gpio": 0,
"reset_gpio": 1,
"reset_active_high": false,
"reset_cycles": 10,
"num_cycles": 2000,
"clock_period_ps": 40000,
"port_mapping": {
"inputs": {
"0": "clk",
"1": "rst_n"
},
"outputs": {
"2": "q_unreset",
"3": "q_reset"
}
},
"reg_init": [
{ "name": "unreset_count", "value": 5, "width": 4 }
]
}
Loading