Skip to content
Open
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
144 changes: 137 additions & 7 deletions dcalc/CcsCeffDelayCalc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,29 @@
#include "Network.hh"
#include "Parasitics.hh"
#include "Scene.hh"
#include "TableModel.hh"
#include "TimingArc.hh"
#include "Units.hh"

namespace sta {

// OpenROAD-fork: ccs-receiver -- process-global toggle (see header). Default
// OFF: ccs_ceff is byte-identical to the constant-Cp behavior until the
// OpenROAD set_ccs_delay_calc -receiver_model flag flips it on.
static bool ccs_receiver_model_enabled = false;

void
CcsCeffDelayCalc::setReceiverModelEnabled(bool enabled)
{
ccs_receiver_model_enabled = enabled;
}

bool
CcsCeffDelayCalc::receiverModelEnabled()
{
return ccs_receiver_model_enabled;
}

// Implementaion based on:
// "Gate Delay Estimation with Library Compatible Current Source Models
// and Effective Capacitance", D. Garyfallou et al,
Expand Down Expand Up @@ -94,6 +112,7 @@ CcsCeffDelayCalc::gateDelay(const Pin *drvr_pin,
parasitics_ = scene->parasitics(min_max);
parasitic_ = parasitic;
output_waveforms_ = nullptr;
last_ceff_ = -1.0; // OpenROAD-fork: ccs-delay2 -- reset; set on CCS path.
Comment on lines 112 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The member variable drvr_pin_ is never initialized or assigned in gateDelay(). However, it is subsequently used on line 161 (pinPvt(drvr_pin_, scene, min_max)) and in watchWaveform() (line 617). This results in undefined behavior or crashes due to dereferencing an uninitialized or stale pointer. Assign drvr_pin_ = drvr_pin; at the beginning of gateDelay() to resolve this.

  drvr_pin_ = drvr_pin;
  parasitics_ = scene->parasitics(min_max);
  parasitic_ = parasitic;
  output_waveforms_ = nullptr;
  last_ceff_ = -1.0;  // OpenROAD-fork: ccs-delay2 -- reset; set on CCS path.


const GateTableModel *table_model = arc->gateTableModel(scene, min_max);
if (table_model && parasitic) {
Expand Down Expand Up @@ -125,7 +144,12 @@ CcsCeffDelayCalc::gateDelay(const Pin *drvr_pin,
drvr_rf_->shortName());

double gate_delay, drvr_slew;
// OpenROAD-fork: ccs-receiver -- gather region-dependent receiver caps
// before the Ceff solve. No-op when the flag is off.
initReceiverModel(load_pin_index_map, scene, min_max);
gateDelaySlew(drvr_library, gate_delay, drvr_slew);
// OpenROAD-fork: ccs-delay2 -- expose the converged effective cap.
last_ceff_ = region_ceff_[0];
debugPrint(debug_, "ccs_dcalc", 2, "gate_delay {} drvr_slew {}",
delayAsString(gate_delay, this), delayAsString(drvr_slew, this));

Expand Down Expand Up @@ -156,6 +180,100 @@ CcsCeffDelayCalc::gateDelay(const Pin *drvr_pin,
load_pin_index_map, scene, min_max);
}

// OpenROAD-fork: ccs-receiver -- gather the region-dependent receiver caps for
// the load pins. The CCS receiver_capacitance model lives on the load pin's
// GateTableModel (keyed by the load pin's input transition rf), the same place
// the additive report (CcsReceiverReport.cc) reads it. We sum, over every load
// pin that carries a receiver model:
// - its NLDM static pin cap (already folded into the pi-model far cap c1_),
// - segment 0 (Cr1, active/Miller region) receiver cap,
// - segment 1 (Cr2, settled region) receiver cap,
// evaluated at the driver output transition slew (== the load pin input slew)
// and the stage load cap (only used by 2D receiver-cap tables). Pins without a
// receiver model are left out so they keep their constant NLDM contribution.
// When the flag is off this clears the accumulators (no behavior change).
void
CcsCeffDelayCalc::initReceiverModel(const LoadPinIndexMap &load_pin_index_map,
const Scene *scene,
const MinMax *min_max)
{
recv_has_model_ = false;
recv_nldm_cap_ = 0.0;
recv_cr1_cap_ = 0.0;
recv_cr2_cap_ = 0.0;
if (!ccs_receiver_model_enabled)
return;

const float drvr_slew_f = static_cast<float>(in_slew_);
for (const auto &[load_pin, load_idx] : load_pin_index_map) {
const LibertyPort *load_port = network_->libertyPort(load_pin);
if (load_port == nullptr)
continue;
const LibertyCell *load_cell = load_port->libertyCell();
if (load_cell == nullptr)
continue;
// The receiver cap is indexed by the load pin's input transition, which
// equals the driver output transition for this stage.
const RiseFall *rf = drvr_rf_;
const ReceiverModel *receiver = nullptr;
const TimingArcSetSeq &arc_sets = load_cell->timingArcSetsFrom(load_port);
for (TimingArcSet *arc_set : arc_sets) {
TimingModel *model = arc_set->model(rf);
auto *gate_model = dynamic_cast<GateTableModel*>(model);
if (gate_model == nullptr)
continue;
const ReceiverModel *rm = gate_model->receiverModel();
if (rm && rm->hasCapacitanceModel(0, rf)) {
receiver = rm;
break;
}
}
if (receiver == nullptr)
// No receiver model on this pin: leave it folded into c1_ (fallback).
continue;

const float nldm_cap = load_port->capacitance(rf, min_max);
const float cr1 = receiver->capacitance(0, rf, drvr_slew_f, load_cap_);
// Segment 1 (Cr2) is optional; fall back to Cr1 when absent.
const float cr2 = receiver->hasCapacitanceModel(1, rf)
? receiver->capacitance(1, rf, drvr_slew_f, load_cap_)
: cr1;
recv_has_model_ = true;
recv_nldm_cap_ += nldm_cap;
recv_cr1_cap_ += cr1;
recv_cr2_cap_ += cr2;
debugPrint(debug_, "ccs_dcalc", 2,
"receiver model {} nldm {} cr1 {} cr2 {}",
network_->pathName(load_pin),
capacitance_unit_->asString(nldm_cap),
capacitance_unit_->asString(cr1),
capacitance_unit_->asString(cr2));
}
(void) scene;
}

// OpenROAD-fork: ccs-receiver -- far-cap (c1) value used for region/segment
// seg_idx. With the receiver model off (or no contributing pin) this returns
// the unmodified pi-model far cap c1_, so the charge integral is byte-identical
// to the constant-Cp solve. With it on, the receiver pins' constant NLDM cap is
// removed from c1_ and replaced with the region-appropriate value: Cr1 for
// segments below the receiver threshold (region_vth_idx_, the Miller/transition
// region) and Cr2 at/above it (settled region).
double
CcsCeffDelayCalc::regionFarCap(size_t seg_idx) const
{
if (!ccs_receiver_model_enabled || !recv_has_model_)
return c1_;
const double recv_region_cap =
(seg_idx < region_vth_idx_) ? recv_cr1_cap_ : recv_cr2_cap_;
double c1 = c1_ - recv_nldm_cap_ + recv_region_cap;
// Guard against a pathological library where removing the NLDM cap would
// drive the far cap non-positive; keep the constant behavior in that case.
if (c1 <= 0.0)
return c1_;
return c1;
}

void
CcsCeffDelayCalc::gateDelaySlew(const LibertyLibrary *drvr_library,
// Return values.
Expand Down Expand Up @@ -193,11 +311,16 @@ CcsCeffDelayCalc::gateDelaySlew(const LibertyLibrary *drvr_library,
// Note that eqn 8 in the ref'd paper does not properly account
// for the charge on c1 from previous segments so it does not
// work well.
// OpenROAD-fork: ccs-receiver -- regionFarCap() returns c1_ unchanged
// when the receiver model is off, so this path is byte-identical by
// default; when on, c1 is the region-appropriate receiver-cap-refined
// far cap (Cr1 in the Miller region, Cr2 once settled).
const double c1_region = regionFarCap(i);
double c1_v1, c1_v2, ignore;
vLoad(t1, rpi_ * c1_, c1_v1, ignore);
vLoad(t2, rpi_ * c1_, c1_v2, ignore);
double q1 = seg_v1 * c2_ + c1_v1 * c1_;
double q2 = seg_v2 * c2_ + c1_v2 * c1_;
vLoad(t1, rpi_ * c1_region, c1_v1, ignore);
vLoad(t2, rpi_ * c1_region, c1_v2, ignore);
double q1 = seg_v1 * c2_ + c1_v1 * c1_region;
double q2 = seg_v2 * c2_ + c1_v2 * c1_region;
double ceff = (q2 - q1) / (seg_v2 - seg_v1);

debugPrint(debug_, "ccs_dcalc", 2, "ceff {}",
Expand Down Expand Up @@ -665,14 +788,21 @@ CcsCeffDelayCalc::reportGateDelay(const Pin *drvr_pin,
{
Parasitic *pi_elmore = nullptr;
const RiseFall *rf = arc->toEdge()->asRiseFall();
if (parasitic && !parasitics_->isPiElmore(parasitic)) {
// OpenROAD-fork: ccs-delay-path -- reportGateDelay() can be invoked directly
// (e.g. by report_dcalc) without a preceding gateDelay() call, so the
// parasitics_ member is unset/stale here. Resolve it from the scene like
// gateDelay() does, otherwise the parasitics_ deref below crashes (Signal 11).
parasitics_ = scene->parasitics(min_max);
if (parasitics_ && parasitic && !parasitics_->isPiElmore(parasitic)) {
// Use the drvr_pin PARAMETER, not the stale drvr_pin_ member.
pi_elmore =
parasitics_->reduceToPiElmore(parasitic, drvr_pin_, rf, scene, min_max);
parasitics_->reduceToPiElmore(parasitic, drvr_pin, rf, scene, min_max);
}
std::string report =
table_dcalc_->reportGateDelay(drvr_pin, arc, in_slew, load_cap, pi_elmore,
load_pin_index_map, scene, min_max, digits);
parasitics_->deleteDrvrReducedParasitics(drvr_pin);
if (parasitics_)
parasitics_->deleteDrvrReducedParasitics(drvr_pin); // OpenROAD-fork: ccs-delay-path
return report;
}

Expand Down
45 changes: 45 additions & 0 deletions dcalc/CcsCeffDelayCalc.hh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ public:
const MinMax *min_max,
int digits) override;

// OpenROAD-fork: ccs-delay2 -- read-only accessor exposing the effective
// capacitance (region 0) that the most recent successful CCS gateDelay()
// converged to. Returns a negative value if the last gateDelay() fell back
// to the NLDM table calculator (no CCS waveforms / out-of-bounds / no
// pi-model parasitic). Purely additive: does not affect any delay value or
// the active delay path.
double effectiveCapacitance() const override { return last_ceff_; }

// OpenROAD-fork: ccs-receiver -- process-global toggle for the CCS
// receiver-capacitance model in the Ceff/charge solve. Default OFF so the
// ccs_ceff engine is byte-identical to the constant-Cp behavior. When ON,
// the per-region far-cap contribution of any load pin that carries a CCS
// receiver_capacitance model is swapped from its constant NLDM pin cap to
// the library's region-dependent receiver cap (Cr1 in the active/Miller
// region below the receiver threshold, Cr2 in the settled region). Pins
// without a receiver model keep their constant NLDM contribution. The flag
// is static because the active ArcDelayCalc is selected by name and copied
// per-thread; a process-global toggle keeps every copy consistent without
// threading new state through the copy ctor.
static void setReceiverModelEnabled(bool enabled);
static bool receiverModelEnabled();

// Record waveform for drvr/load pin.
void watchPin(const Pin *pin) override;
void clearWatchPins() override;
Expand Down Expand Up @@ -148,6 +170,29 @@ protected:
Region region_ramp_times_;
Region region_ramp_slopes_;
bool vl_fail_{false};
// OpenROAD-fork: ccs-delay2 -- last converged effective cap (region 0), or
// a negative sentinel when the previous gateDelay() used the NLDM fallback.
double last_ceff_{-1.0};
// OpenROAD-fork: ccs-receiver -- per-solve receiver-capacitance contributions
// summed over the load pins that carry a CCS receiver_capacitance model.
// recv_has_model_ is true when at least one load pin contributed; when false
// the Ceff solve falls back to the constant-Cp behavior (byte-identical).
// recv_nldm_cap_ : sum of the NLDM static pin caps of those pins (the
// quantity already folded into c1_, to be removed).
// recv_cr1_cap_ : sum of segment-0 (Cr1) receiver caps (active/Miller).
// recv_cr2_cap_ : sum of segment-1 (Cr2) receiver caps (settled region).
// These are only consulted when receiverModelEnabled() is true.
bool recv_has_model_{false};
double recv_nldm_cap_{0.0};
double recv_cr1_cap_{0.0};
double recv_cr2_cap_{0.0};
// Compute the per-segment far-cap (c1) value for region/segment seg_idx,
// applying the receiver model when enabled; otherwise returns c1_.
double regionFarCap(size_t seg_idx) const;
// Populate recv_* from the load pins; no-op (clears) when the flag is off.
void initReceiverModel(const LoadPinIndexMap &load_pin_index_map,
const Scene *scene,
const MinMax *min_max);
// Waveform recording.
WatchPinValuesMap watch_pin_values_;

Expand Down
19 changes: 19 additions & 0 deletions dcalc/DelayCalc.i
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "DelayCalc.hh"
#include "Sta.hh"
#include "dcalc/ArcDcalcWaveforms.hh"
#include "dcalc/CcsCeffDelayCalc.hh"
#include "dcalc/PrimaDelayCalc.hh"

%}
Expand All @@ -54,6 +55,24 @@ set_delay_calculator_cmd(const char *alg)
sta::Sta::sta()->setArcDelayCalc(alg);
}

// OpenROAD-fork: ccs-receiver -- toggle the CCS receiver-capacitance model in
// the ccs_ceff Ceff solve. Process-global; default OFF (byte-identical to the
// constant-Cp behavior). Selecting/deselecting the ccs_ceff engine is done
// separately via set_delay_calculator_cmd; this only flips the receiver-model
// sub-behavior. delaysInvalid() so the next query re-solves with the new mode.
void
set_ccs_receiver_model_enabled(bool enabled)
{
sta::CcsCeffDelayCalc::setReceiverModelEnabled(enabled);
sta::Sta::sta()->delaysInvalid();
}

bool
ccs_receiver_model_enabled()
{
return sta::CcsCeffDelayCalc::receiverModelEnabled();
}

void
set_delay_calc_incremental_tolerance(float tol)
{
Expand Down
6 changes: 6 additions & 0 deletions include/sta/ArcDelayCalc.hh
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ public:
const MinMax *min_max,
int digits) = 0;
virtual void finishDrvrPin() = 0;

// OpenROAD-fork: ccs-delay2 -- effective capacitance the most recent
// gateDelay() converged to. Meaningful only for CCS Ceff calculators; the
// base returns a negative sentinel (no Ceff / not a Ceff-solving calc).
// Read-only; does not affect any delay value or the active delay path.
virtual double effectiveCapacitance() const { return -1.0; }
};

} // namespace sta
18 changes: 18 additions & 0 deletions include/sta/TableModel.hh
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,25 @@ public:
const RiseFall *rf);
static bool checkAxes(const TableModel *table);

// OpenROAD-fork: ccs-delay -- read-only accessors so the OpenROAD (dbSta)
// layer can SURFACE the CCS receiver-capacitance that OpenSTA already
// parses but never evaluates. These do NOT participate in delay calc; the
// NLDM delay path is unchanged. See src/dbSta/src/CcsReceiverReport.cc.
//
// hasCapacitanceModel: true if segment/rf has a parsed receiver-cap table.
// capacitance: interpolated receiver capacitance (library cap units) at the
// given input transition (in_slew) and, for 2D tables, output net cap
// (out_cap). segment 0 = receiver_capacitance1 (Cr1), 1 = Cr2.
bool hasCapacitanceModel(size_t segment, const RiseFall *rf) const;
float capacitance(size_t segment,
const RiseFall *rf,
float in_slew,
float out_cap) const;

private:
const TableModel *capacitanceModel(size_t segment,
const RiseFall *rf) const;

std::vector<TableModel> capacitance_models_;
};

Expand Down
44 changes: 44 additions & 0 deletions liberty/TableModel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,50 @@ ReceiverModel::checkAxes(const TableModel *table)
&& axis3 == nullptr);
}

// OpenROAD-fork: ccs-delay -- read-only accessors. These evaluate the CCS
// receiver-capacitance tables that OpenSTA already parses into
// capacitance_models_ but never consumes. They are used only by the additive
// OpenROAD report (report_ccs_receiver_delta); the NLDM delay path does not
// call them, so default/flag-off behavior is byte-identical to baseline.
const TableModel *
ReceiverModel::capacitanceModel(size_t segment,
const RiseFall *rf) const
{
if (rf == nullptr)
return nullptr;
const size_t idx = segment * RiseFall::index_count + rf->index();
Comment on lines +457 to +460

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the rf parameter is passed as nullptr, calling rf->index() will dereference a null pointer and crash. Adding a defensive null check at the beginning of capacitanceModel() will safely protect this method and its callers (hasCapacitanceModel() and capacitance()).

{
  if (rf == nullptr)
    return nullptr;
  const size_t idx = segment * RiseFall::index_count + rf->index();

if (idx >= capacitance_models_.size())
return nullptr;
const TableModel &model = capacitance_models_[idx];
// Empty (default-constructed) slots have no backing table.
if (model.table() == nullptr)
return nullptr;
return &model;
}

bool
ReceiverModel::hasCapacitanceModel(size_t segment,
const RiseFall *rf) const
{
return capacitanceModel(segment, rf) != nullptr;
}

float
ReceiverModel::capacitance(size_t segment,
const RiseFall *rf,
float in_slew,
float out_cap) const
{
const TableModel *model = capacitanceModel(segment, rf);
if (model == nullptr)
return 0.0f;
// ReceiverModel::checkAxes guarantees axis1 is input_net_transition (1D) or
// input_net_transition x total_output_net_capacitance (2D, canonicalized).
// Table::findValue ignores trailing args for lower-order tables, so passing
// (in_slew, out_cap, 0) is correct for both 1D and 2D receiver-cap tables.
return model->findValue(in_slew, out_cap, 0.0f);
}

////////////////////////////////////////////////////////////////

CheckTableModel::CheckTableModel(LibertyCell *cell,
Expand Down