diff --git a/dcalc/CcsCeffDelayCalc.cc b/dcalc/CcsCeffDelayCalc.cc index 9de98d81..39eb5c61 100644 --- a/dcalc/CcsCeffDelayCalc.cc +++ b/dcalc/CcsCeffDelayCalc.cc @@ -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, @@ -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. const GateTableModel *table_model = arc->gateTableModel(scene, min_max); if (table_model && parasitic) { @@ -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)); @@ -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(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(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. @@ -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 {}", @@ -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; } diff --git a/dcalc/CcsCeffDelayCalc.hh b/dcalc/CcsCeffDelayCalc.hh index 90f27561..3e5a097e 100644 --- a/dcalc/CcsCeffDelayCalc.hh +++ b/dcalc/CcsCeffDelayCalc.hh @@ -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; @@ -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_; diff --git a/dcalc/DelayCalc.i b/dcalc/DelayCalc.i index 69e23059..5467ee37 100644 --- a/dcalc/DelayCalc.i +++ b/dcalc/DelayCalc.i @@ -30,6 +30,7 @@ #include "DelayCalc.hh" #include "Sta.hh" #include "dcalc/ArcDcalcWaveforms.hh" +#include "dcalc/CcsCeffDelayCalc.hh" #include "dcalc/PrimaDelayCalc.hh" %} @@ -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) { diff --git a/include/sta/ArcDelayCalc.hh b/include/sta/ArcDelayCalc.hh index eacad445..8e92b767 100644 --- a/include/sta/ArcDelayCalc.hh +++ b/include/sta/ArcDelayCalc.hh @@ -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 diff --git a/include/sta/TableModel.hh b/include/sta/TableModel.hh index 40855a75..7ab1bedf 100644 --- a/include/sta/TableModel.hh +++ b/include/sta/TableModel.hh @@ -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 capacitance_models_; }; diff --git a/liberty/TableModel.cc b/liberty/TableModel.cc index 5cdd92f2..327ed010 100644 --- a/liberty/TableModel.cc +++ b/liberty/TableModel.cc @@ -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(); + 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,