logicaffeine_compile/codegen_sva/flow_config.rs
1//! `flow_config` — the compiler emits its own physical-design configuration.
2//!
3//! WHY THIS EXISTS. Closing a block's electrical gates was, until now, a person editing environment
4//! variables in front of `scripts/openlane-run.sh` — a slew margin here, an antenna threshold
5//! there, a density sweep — and the knowledge of WHICH values work lived in that person's head and
6//! in a scratch directory. Every new block re-learned it. The whole-program engine, which does not
7//! exist as a hardened macro yet, would have re-learned it again.
8//!
9//! The compiler already knows the design and (via [`super::pdk_target::PdkTarget`]) the process.
10//! That is everything needed to choose the flow configuration, so it should choose it — and the
11//! measured reason for each value should live next to the value, in code, where a test can read it.
12//!
13//! EVERY NUMBER BELOW IS MEASURED, on this box, and the measurement is quoted at its field. Two of
14//! them are NEGATIVE results kept deliberately, because the expensive mistake is re-trying a lever
15//! that has already been shown not to work.
16
17use super::pdk_target::{HarnessClock, PdkTarget, PdkUnsupported};
18use std::fmt::Write as _;
19
20/// What the compiler knows about a design that changes how it should be built.
21#[derive(Debug, Clone, PartialEq)]
22pub struct DesignProfile {
23 /// Top-level module name; also the OpenLane `DESIGN_NAME`.
24 pub name: String,
25 pub clock_port: String,
26 pub clock_period_ns: f64,
27 /// Top-level I/O count. A design with many wide buses is PAD-LIMITED — its die is set by the
28 /// perimeter its pins need, not by its logic — and must be given an explicit die area or the
29 /// floorplanner sizes it too small for the pins to fit.
30 pub port_bits: u32,
31 /// Hardened macros this design instantiates (SRAM, or another of our blocks).
32 pub macros: Vec<String>,
33 /// Set for a fixed-die shuttle vehicle wrapper, where the die is not ours to choose.
34 pub fixed_die: bool,
35}
36
37impl DesignProfile {
38 pub fn new(name: &str, clock_port: &str, clock_period_ns: f64) -> Self {
39 Self {
40 name: name.to_string(),
41 clock_port: clock_port.to_string(),
42 clock_period_ns,
43 port_bits: 0,
44 macros: Vec::new(),
45 fixed_die: false,
46 }
47 }
48 pub fn with_ports(mut self, bits: u32) -> Self {
49 self.port_bits = bits;
50 self
51 }
52 /// A design is pad-limited when its perimeter, not its logic, sets the die. The threshold is
53 /// the point at which the observed blocks needed an explicit `DIE_AREA`: the infinichip
54 /// subsystem (~386 pins) did, everything smaller did not.
55 pub fn is_pad_limited(&self) -> bool {
56 self.port_bits >= 256
57 }
58}
59
60/// How antenna violations are to be closed. These are NOT interchangeable, and choosing wrongly
61/// costs slew.
62///
63/// MEASURED on `coproc_core`: its max-slew stuck at exactly 126 whether the resizer margin was 30
64/// or 45 — the lever could not reach the cause. Half the slew violators were `ANTENNA_*/DIODE`
65/// pins. A diode is extra CAPACITANCE on its net, and the diode passes run AFTER the resizer, so
66/// slew repair never sees the load they add. Diode-based antenna closure and slew closure fight.
67///
68/// The SAT jog-router fixes antenna with METAL instead — it bridges a segment one layer up, adding
69/// no cell and no capacitance — so on a block where slew is tight it is strictly the better tool.
70/// It only became safe to rely on alone once its free-window search was taught the layer spacing
71/// rule; before that it placed bridges flush against foreign metal (359 Magic violations on the
72/// Caravel wrapper, while TritonRoute reported 0).
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum AntennaStrategy {
75 /// OpenLane's diode passes plus the jog ECO. The default: it closed `gpu_simt` and
76 /// `fabric_soc` outright, and diodes are cheap when slew has headroom.
77 DiodesAndJog,
78 /// Jog-router only — no diode is inserted anywhere. For a block whose slew is tight, where the
79 /// diodes' added load is the thing keeping slew from closing.
80 JogOnly,
81 /// The PRE-ROUTE heuristic diode pass plus the jog ECO, with OpenLane's POST-route
82 /// `GRT_REPAIR_ANTENNAS` off.
83 ///
84 /// This exists because the other two variants are not the only choices, and treating them as
85 /// such hid the best one. OpenLane runs TWO independent diode passes — a pre-route heuristic
86 /// pass driven by `HEURISTIC_ANTENNA_THRESHOLD`, and the global router's own post-route repair
87 /// — and an enum with only "both" and "neither" cannot express the third combination. Every
88 /// `coproc_core` experiment for two sessions moved them together:
89 ///
90 /// | heuristic | GRT repair | result |
91 /// |---|---|---|
92 /// | on | on | 17,716 diodes, max-slew 126-282 |
93 /// | off | off | antenna 538-770 violating nets |
94 /// | on | off | **slew 120, antenna 27, and it ROUTES** |
95 ///
96 /// That third row is this variant: DRC 0, LVS 0, XOR 0, routing 0, setup +32.54, hold +0.27 on
97 /// a block previously recorded as a dead end needing an emitter-side rewrite. What made the
98 /// difference is not the diode COUNT — that is essentially unchanged at 17,548 — but that
99 /// GRT's repair RE-ROUTES as well as inserting, and it was that re-route, not the diodes,
100 /// that hit GRT-0119 congestion at every utilisation and die size tried.
101 ///
102 /// It also corrected a wrong attribution: the diodes were believed to come from GRT because
103 /// they "survived" a threshold of 300. Surviving a threshold does not identify a source. With
104 /// GRT off there are still 17,548 of them, so they come from the heuristic pass — which makes
105 /// `heuristic_antenna_threshold` the live lever rather than an exhausted one. (Though not a
106 /// wide one: at 1500 the count drops far enough that antenna explodes back to 510 nets. It is
107 /// a cliff, not a gradient.)
108 HeuristicDiodesAndJog,
109}
110
111/// The electrical-closure settings, separated from the rest because they are the ones that were
112/// actually measured and the ones a future change is most likely to get wrong.
113#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct ClosureSettings {
115 /// The ABC script technology mapping runs, or OpenLane's default (`AREA 0`) when `None`.
116 ///
117 /// MEASURED on `wpe_user_project` (191,641 generic gates between 32,779 inputs and 32,773 outputs, the
118 /// on-die tables' read multiplexers most of them): `AREA 0` opens with `fx; mfs`, and its ABC pass had
119 /// run 2 h 15 min without finishing when it was stopped (RUN_2026.09.25_19.58.43). `AREA 3`
120 /// (`strash; dch; map`) mapped the same extracted netlist in 6 min 8 s, to 240,032 cells and
121 /// 1.86 mm² of logic with an 8.56 ns critical path against a 40 ns clock.
122 pub synth_strategy: Option<SynthStrategy>,
123 /// Percent margin `repair_design` targets below the slew limit.
124 ///
125 /// MEASURED on `gpu_simt` (79,965 cells): at OpenLane's defaults (PL 20 / GLB 10) the block
126 /// signed off carrying **6 max-slew violations**. At **30** it reached slew 0 — and the design
127 /// got better in every other respect at the same time: max-fanout 3,999 -> 847, setup slack
128 /// +3.75 -> +4.48 ns, cell count 79,965 -> 69,080. Buffering for slew shortens the electrical
129 /// paths that were forcing large drivers elsewhere.
130 ///
131 /// IT INTERACTS WITH THE CLOCK-DRIVER DECLARATION, and the interaction is large. Declaring
132 /// `SYNTH_CLK_DRIVING_CELL` (see `PdkTarget::clk_driving_cell`) makes the clock port's assumed
133 /// edge FAST, which removes the pessimism that was inflating every downstream transition — and
134 /// with it the resizer's reason to buffer. On `systolic_os_pipe8_realbist` (392k cells) the two
135 /// nearly-identical runs differ only in that setting: slew went 3 -> 89, all 89 of them
136 /// MARGINAL (0.93-1.01 against a 0.75 limit) and the design 3k cells SMALLER. At margin 45 the
137 /// same configuration reached slew 0, with setup +2.03 and XOR back to 0. So the clock fix is
138 /// right and needs the margin raised alongside it, not reverted.
139 ///
140 /// 45 IS NOT A SAFE DEFAULT. The placement resizer destabilises at high margin in a
141 /// size-dependent way: measured OK at 45 on 417k and 392k cells, CRASHES at 45 on 819k and
142 /// 204k, and crashes at 60 on 417k. Raise it per block against a measured failure, not
143 /// globally.
144 ///
145 /// AND THE FAILURE MODE AT SIZE IS MEMORY, NOT A CRASH — which matters because it does not
146 /// look like a failure. On `systolic_pipe16_realbist` (457k cells) at margin 45 the
147 /// PLACEMENT RESIZER alone reached 92.9 GB resident after 38 minutes, still at step 9, with
148 /// detailed routing — the real peak — ahead of it. The kernel had already taken one openroad
149 /// at 98.7 GB on this 125 GB box that day, and an OOM kill leaves an EMPTY `.errors` file and
150 /// a log that simply stops, so it reads as a mystery crash rather than as this setting. Three
151 /// runs were lost that way before the cause was identified. At margin 30 the same block
152 /// completes.
153 ///
154 /// So the size dependence is a memory curve with a cliff, and 457k cells sits just past the
155 /// safe edge: 392k and 417k are fine at 45, 457k is not. Between "works" and "OOM" there is
156 /// no warning, because the resizer gives none.
157 ///
158 /// MARGIN 35 ALSO OOMs ON 457k. The cliff is not between 35 and 45; it is below 35, and only
159 /// the default 30 completes on that block. Measured with a 45-SECOND SAMPLER the peak read
160 /// 83 GB — comfortably under the wall — while the kernel still took the process, because a
161 /// sampler misses the spike between polls. Read `/proc/<pid>/status` VmHWM for the true
162 /// high-water mark; a sampled peak is an underestimate and will tell you a run was safe when
163 /// it was not.
164 /// THIS DRIVES THE ROUTING-STAGE (GLB) RESIZER ONLY. It used to drive the placement (PL)
165 /// resizer too, and that coupling is a trap: the two have DIFFERENT failure modes, so
166 /// raising the one that helps necessarily raised the one that explodes.
167 /// GLB (step 15, `repair_design` after global route) is where the slew repair that closed
168 /// `systolic_os_pipe8_realbist` happened — 89 -> 0 at margin 45.
169 /// PL (step 9, before CTS) is where the memory goes. On `systolic_pipe16_realbist` (457k
170 /// cells) it reached a TRUE peak of 114 GB (VmHWM) at margin 35 on a 125 GB box and was
171 /// OOM-killed; margin 45 and margin 35-with-fewer-diodes did the same.
172 /// Split so a block can take the slew repair it needs without the placement resizer that
173 /// cannot afford it. See `pl_slew_margin_pct`.
174 pub slew_margin_pct: u32,
175 /// Margin for the PLACEMENT resizer (step 9). Kept at the safe default independently of
176 /// `slew_margin_pct` because this is the one with the memory cliff — measured OK at 45 on
177 /// 392k and 417k cells, OOM at 35 on 457k, OOM at 45 on 819k.
178 ///
179 /// A NOTE ON MEASURING IT: a sampled peak is not a peak. A 45-second sampler read 83 GB on a
180 /// run whose true VmHWM was 114 GB, i.e. it reported "comfortably under the wall" about a
181 /// process the kernel was killing. Read /proc/<pid>/status VmHWM.
182 pub pl_slew_margin_pct: u32,
183 /// Percent margin below the capacitance limit. Same mechanism, same call
184 /// (`repair_design -cap_margin`); `fabric_horner_seq` is the only block with nonzero max-cap
185 /// (7), so this is carried at the same value rather than tuned separately.
186 pub cap_margin_pct: u32,
187 /// Fanout above which the heuristic diode pass inserts a protection diode.
188 ///
189 /// THE RELATIONSHIP IS NOT MONOTONIC, and reading it as monotonic wastes runs in both
190 /// directions. It is a U-curve whose optimum depends on how large the residual already is.
191 ///
192 /// AT A LARGE RESIDUAL, RAISE IT. A low threshold floods the design with diodes that crowd
193 /// out the ones that would have mattered — post-fill there is no free site left for the
194 /// diode the ARC check actually wants. Measured: `gpu_simt` 10 -> 300 took antenna 4 -> 2;
195 /// `coproc_core` residual 15 at 300 against 23 at 160; `systolic_os_pipe8_realbist` built at
196 /// 10 came out 536,104 cells with 8 max-slew violations against 392,221 cells with 3 at 300.
197 /// The flood is not subtle at the bottom end: `soft_core_scalar` at threshold 10 inserted
198 /// 7,527 diodes into a 14,700-cell design — over half the instances — and died at GRT-0119.
199 ///
200 /// AT A SMALL RESIDUAL, LOWER IT. Once the count is down to single digits the remaining
201 /// violators are often nets the pass simply never selected, and there is no flooding risk
202 /// left to avoid. Measured the same afternoon, on the same blocks that produced the numbers
203 /// above: `systolic_os_pipe8_realbist` 300 -> 150 took antenna 3 -> 0 and CLOSED the block;
204 /// `coproc_core` 300 -> 150 took 5 -> 2 with max-slew still 0.
205 ///
206 /// So the default stays 300 because a block's FIRST run is always in the large-residual
207 /// regime, and 150 is the endgame move once the count is small and a diode-less violator is
208 /// visible in the report. Both directions are real; neither generalises.
209 ///
210 /// AND IT CANNOT FIX EVERYTHING. A violator whose pin ALREADY carries a diode is beyond this
211 /// lever entirely — measured on `coproc_core`, where all 7 survivors had one. That case needs
212 /// `grt_antenna_margin_pct` (which acts before the blocking geometry exists), a base-route
213 /// change, or narrower RTL.
214 pub heuristic_antenna_threshold: u32,
215 /// Iterations of the SAT jog-router antenna ECO.
216 ///
217 /// MEASURED on `gpu_simt`: the ECO converges 8 -> 2 -> 1 in its first THREE iterations and
218 /// then sits at 1 for twenty-eight more. So this is sized for convergence, not for grinding;
219 /// raising it further buys nothing. A residual that survives here is a side-area violation,
220 /// which no number of diode iterations can fix.
221 pub antenna_eco_iters: u32,
222 /// Placement density.
223 ///
224 /// NEGATIVE RESULT, recorded so it is not re-tried: 0.40 is WORSE than 0.45 for `gpu_simt`
225 /// (antenna 4 vs 2, cells 43,952 vs 43,564) even though 0.40 is exactly what finally cleared
226 /// `coproc_core`'s last violator. The difference is the violation's KIND — coproc's residual
227 /// was a met5 routing detour that a different base route removed, gpu_simt's is a met1
228 /// side-area violation that a density change does not touch. Sweep density only against an
229 /// upper-layer detour.
230 pub target_density: f64,
231 /// Global-route cost penalty on met1, 0..1 (higher = the router avoids met1 more).
232 ///
233 /// MEASURED — this is what finally closed `gpu_simt`. After slew/cap margin and the diode
234 /// threshold, one met1 SIDE-AREA antenna violator survived at ratio 1.14. It could not be
235 /// dioded (a diode addresses gate area; side area is perimeter with no diffusion term), the
236 /// jog router plateaued on it (8 -> 2 -> 1 in three ECO iterations, then 1 for twenty-eight
237 /// more), and a density sweep made it WORSE. Forbidding met1 for signals outright
238 /// (`RT_MIN_SIGNAL_LAYER=met2`) FAILED TO ROUTE even at 25% core utilisation — the design
239 /// needs met1 capacity. A cost penalty is the version that works: met1 stays available so it
240 /// cannot congest, but the router stops building long met1 runs. At **0.7** the block reached
241 /// antenna 0/0 with every other gate still at zero.
242 pub met1_route_penalty: f64,
243 /// The rip-up iterations global routing spends removing overflow (`GRT_OVERFLOW_ITERS`), or OpenLane's own 50 when
244 /// `None`. Every global route of the flow takes it, the resizers' re-routes and the fanout step's included.
245 ///
246 /// MEASURED on `wpe_user_project` at 16 ns (the private tree's RUN_2026.10.19_19.38.51 and its variants): at 50 the
247 /// route stopped at GRT-0119 with gcells at the die center over by one to three tracks; at 150 and at 400, every
248 /// other setting held, it stopped the same way, the iterations ending once the overflow could no longer grow
249 /// (GRT-0230; RUN_2026.10.19_20.34.27, RUN_2026.10.19_22.07.52). Iterations alone do not route a block whose
250 /// gcells are over capacity; with capacity given back ([`ClosureSettings::grt_adjustment`] or
251 /// [`ClosureSettings::met1_route_penalty`]) the same block routed at 150 with no overflow.
252 pub grt_overflow_iters: Option<u32>,
253 /// The share of every gcell's capacity global routing holds back, 0..1 (`GRT_ADJUSTMENT`), or OpenLane's own 0.3
254 /// when `None`. The reserve is room detailed routing has for vias, pins and blockages the global grid does not see;
255 /// a block that cannot route under it trades some of that room for capacity.
256 ///
257 /// MEASURED on `wpe_user_project` at 16 ns, 150 overflow iterations: at 0.3 the global route stopped at GRT-0119
258 /// (RUN_2026.10.19_20.34.27); at 0.2 it routed with no overflow, 18,902,039 um (RUN_2026.10.20_17.57.25, x5).
259 pub grt_adjustment: Option<f64>,
260 /// The sites a side every cell keeps free of its neighbours when placement is legalized (`DPL_CELL_PADDING`, which
261 /// OpenLane takes as a total and halves to a side), or OpenLane's none when `None`. Abutting cells in a dense patch
262 /// leave detailed routing no way to reach their met1 pins; global placement is left unpadded, so its density target
263 /// still holds.
264 pub dpl_cell_padding_per_side: Option<u32>,
265 /// The longest wire `repair_clock_nets` leaves between a clock input and the tree's root, in um
266 /// (`CTS_CLK_MAX_WIRE_LENGTH`).
267 ///
268 /// MEASURED on `wpe_user_project` (2.8 x 3.4 mm, RUN_2026.09.26_17.51.01, the CTS libraries at every corner):
269 /// OpenROAD's own limit, 3,331 um, left the 1.7 mm run from `wb_clk_i` to the root buffer whole, 1.125 ns at
270 /// its end against the 0.75 ns limit; at 800 um it takes four buffers and no point on it exceeds 0.353 ns —
271 /// under the post-diode upsize's detection limit too. At 1,000 um it reaches 0.429 ns; at 500 um it rewires
272 /// fourteen of the tree's own nets.
273 ///
274 /// 0 keeps OpenROAD's own limit, as every block hardened before this was measured was built
275 /// ([`parse_wire_limit_um`]).
276 pub clock_max_wire_um: u32,
277 /// The longest wire either resizer leaves on a signal net, in um (`PL_RESIZER_MAX_WIRE_LENGTH`,
278 /// `GLB_RESIZER_MAX_WIRE_LENGTH`).
279 ///
280 /// MEASURED on `wpe_user_project` (RUN_2026.09.27_06.33.43): under OpenROAD's own 3,331 um, nets of 1,447 to
281 /// 1,814 um driven by `buf_12` — the strongest buffer the flow may place — signed off at 0.76-0.87 ns against the
282 /// 0.75 ns limit at ss_100C_1v60, max RC, below what the post-diode upsize's global-route estimate detects. 800 um,
283 /// the clock's limit, splits the 2,709 of 310,043 nets routed longer (0.9%).
284 ///
285 /// 0 keeps OpenROAD's own limit ([`parse_wire_limit_um`]).
286 pub signal_max_wire_um: u32,
287 /// Ratio margin for the GLOBAL-ROUTE antenna repair (`repair_antennas -ratio_margin`),
288 /// as a percent. The PDK default is 10.
289 ///
290 /// WHY THIS LEVER IS DIFFERENT FROM EVERY OTHER ANTENNA LEVER, and why it was worth adding:
291 /// it is the ONLY antenna repair that runs BEFORE detailed routing. Everything else in this
292 /// struct acts afterwards, and by then TritonRoute has patched nets with via-enclosure RECTs
293 /// that the jog router cannot bridge (odb exposes no way to round-trip that geometry from
294 /// Python — `getRect` needs out-params, there are no SWIG pointer helpers, and
295 /// `dbWireShapeItr` is unbound; all three checked). A net that reaches detailed routing
296 /// already violating, and already carrying a diode, has NO repair left. This one gets in
297 /// first.
298 ///
299 /// MEASURED on `coproc_core` (180k cells, narrow-multiply RTL): raising 10 -> 50 with
300 /// `grt_antenna_iters` 15 -> 25 took the antenna residual 11 -> 5 with max-slew still 0 and
301 /// every other gate at zero. Nothing else in the run changed. The repair itself is cheap —
302 /// 3 diodes placed at margin 50 against 1 at margin 10 — because global route works from an
303 /// ESTIMATE and only sees the worst offenders; that is also why it cannot be the whole fix.
304 ///
305 /// IT HAS AN OPTIMUM, NOT A DIRECTION. Pushing further on the same block — 50/25 -> 100/40,
306 /// every other setting held — took the residual from 1 net back up to 8, including a new met5
307 /// violator (ratio 1.10 against a 3486.68 requirement). Over-repairing at global route inserts
308 /// diodes and layer changes against an ESTIMATE, and detailed routing then lands elsewhere.
309 /// Measured points on `coproc_core`: 10 -> 11 nets, 50 -> 5 (then 1 with threshold 100),
310 /// 100 -> 8. Stay at 50.
311 pub grt_antenna_margin_pct: u32,
312 /// Iterations for the same pre-detailed-route repair. PDK default 15.
313 pub grt_antenna_iters: u32,
314 /// Insert antenna diodes BEFORE the slew/cap resizer instead of after it
315 /// (`LOGOS_DIODES_BEFORE_RESIZE`, implemented in `scripts/tcl_commands/routing.tcl`).
316 ///
317 /// The base order sizes every driver and THEN hangs diodes on its net, so the resizer never
318 /// sees the load it is sizing for. Measured on two blocks: 25 of `coproc_core`'s 69 slew
319 /// violators were diode pins, and every violating driver net on `systolic_pipe16_realbist`
320 /// carried diodes. Both symptom-side fixes failed — re-repairing after the diodes congested
321 /// or backfired (3,587 buffers for 7 violations, slew 69 -> 181), and raising the resizer
322 /// margins hit a 114 GB OOM in the placement resizer and a 40-minute stall in the routing one.
323 /// Heuristic diode insertion reads placement, not routing, so it can legally run first.
324 ///
325 /// MEASURED NEGATIVE, which is why it stays off. On `systolic_pipe16_realbist` (margins 30/30,
326 /// everything else held) it made max-slew 27 -> 123, antenna 18 -> 25 nets, and squeezed setup
327 /// to +0.42 ns. Sizing drivers for diodes that are already present does not help when the
328 /// diodes themselves are the problem: most of the design's ~18k heuristic diodes are joined to
329 /// their nets above the violating layer and are not credited there at all (see
330 /// `eco_diode_fallback`), so they are load with no antenna benefit.
331 pub diodes_before_resize: bool,
332 /// At the jog router's fixpoint, add ONE more diode at each still-violating pin (the
333 /// manual-stub ECO in `routing.tcl`, `LOGOS_ECO_DIODE_FALLBACK`) instead of stopping.
334 ///
335 /// The jog and the diode ECO were written either/or: with the jog enabled the diode ECO was
336 /// unreachable code, so the nets the jog cannot bridge were exactly the nets nothing repaired.
337 /// Every remaining violator on `coproc_core` and `systolic_pipe16_realbist` had that shape —
338 /// one heuristic diode already on the pin, ratio still 1.0-1.4x, and a via-enclosure RECT the
339 /// jog cannot round-trip. One diode's diffusion is not enough and no pass ever added a second.
340 /// The stub ECO runs no router, so it cannot strand a net; any geometry problem it caused
341 /// would surface at the fail-closed DRC/LVS/XOR gates rather than as a false clean.
342 pub eco_diode_fallback: bool,
343 /// After the diodes are in, grow the driver of each net that violates max slew WITH its diodes
344 /// present, or buffer that one net when its driver is already the strongest of its function
345 /// (`LOGOS_POST_DIODE_UPSIZE`, `scripts/openroad/logos_post_diode_upsize.tcl`).
346 ///
347 /// The surgical successor to the whole-design post-diode repair (3,587 buffers for 7
348 /// violations, slew 69 -> 181). Every surviving slew violator on `systolic_pipe16_realbist`
349 /// was driven by an `a21oi_4`/`a31oi_4` — the top of its family, so no upsize reaches it.
350 /// Measured offline on that block's post-diode netlist: 22 nets selected, 28 drivers grown,
351 /// 15 nets rebuffered, +26 cells of 457k, 0 residual after 7 rounds, about five minutes; the
352 /// three culprit drivers went from 0.41-0.43 ns estimated slew to at most 0.13 ns.
353 pub post_diode_upsize: bool,
354 /// How far below the real limit the upsize DETECTS, in percent. The global-route estimate is
355 /// about 2x optimistic on the nets signoff flags (0.37-0.43 ns estimated vs 0.75-0.85 ns
356 /// signed off) but ranks them correctly: at 52% (0.36 ns) all 15 of pipe16's signoff-violating
357 /// pins are selected among 103 pins on 22 nets; at 0.40 ns two are missed; 0.34 ns selects
358 /// 442 pins and 0.30 ns 2,081. Signoff still judges against the real limit.
359 pub post_diode_upsize_margin_pct: u32,
360 /// Detect the upsize's violators at EVERY library corner (ss / tt / ff), not the typical one
361 /// alone (`LOGOS_UPSIZE_ALL_CORNERS`). Signoff judges slew at ss_100C_1v60; every surviving
362 /// violator on `systolic_pipe16_realbist` and `systolic_os_pipe8_realbist`
363 /// (RUN_2026.09.23_14.05.55 / _11.27.15) was slow-corner only, typical slew 0.
364 pub post_diode_upsize_all_corners: bool,
365 /// Upsize again after global routing's antenna repair, with that repair's diodes present, then re-route without
366 /// it (`LOGOS_POST_ANTENNA_UPSIZE`, `scripts/tcl_commands/routing.tcl`).
367 ///
368 /// `repair_antennas` runs after the post-diode upsize and hangs diodes on nets it has already sized. MEASURED on
369 /// `wpe_user_project` (RUN_2026.09.27_06.33.43): up to 20 on a single-sink net of 255-299 um (ANT-0009, more than
370 /// 10 diodes per gate), and 13 of its 21 slow-corner slew violators were such nets, each driven by the strongest
371 /// cell of its function. Replayed on that run's post-repair route, the upsize found and repaired all 13 (+151
372 /// cells). Off unless chosen: it adds a global route.
373 pub post_antenna_upsize: bool,
374 /// Build the clock tree within the SDC's `set_max_fanout` (`LOGOS_CTS_FANOUT`, `scripts/openroad/cts.tcl` and
375 /// `logos_cts.py`): leaf clusters of at most the limit's sinks, H-tree splits balanced, and the tree deep enough
376 /// that a balanced leaf averages under the limit.
377 ///
378 /// At OpenLane's defaults a leaf cluster may hold 25 sinks and each H-tree split may hand one branch 60% of its
379 /// parent's clusters, so the imbalance compounds level by level. MEASURED on `gpu_simt`'s placement
380 /// (RUN_2026.09.23_09.37.32, CTS replayed): 33 clock nets over a limit of 10 at the defaults; 3 with clusters
381 /// held to 10, every one an H-tree leaf driving 11-12 cluster buffers; 0 with the splits balanced too, from the
382 /// same 317 clock buffers, skew 0.54 -> 0.48 ns. A shallower or deeper unbalanced tree is worse, not better:
383 /// at a maximum of 7 or 5 leaf sinks, 4 nets stayed over (up to 16) and 5 added 160 buffers. Every signed-off
384 /// block's clock violators were these leaves: 703 on `wpe_user_project`, 295 on `systolic_pipe16_realbist`.
385 pub cts_within_fanout: bool,
386 /// Split every net the diode passes take over `set_max_fanout` by whole gates, each with the diodes placed for
387 /// it (`LOGOS_DIODE_FANOUT`, `scripts/openroad/logos_diode_fanout.tcl`), after the pre-route diodes and again
388 /// after global routing's antenna repair; and hold the post-route diode ECO under the limit.
389 ///
390 /// The resizer meets the limit on every logic net — all 23 signed-off blocks had 0 logic nets over it — and
391 /// the liberty counts a diode as a load, so every non-clock violator signoff reported was a diode count:
392 /// 5,052 of `wpe_user_project`'s 5,755 (ten mux2 inputs with a heuristic diode each read as 20; global
393 /// routing's repair put up to 26 on one net), 2,384 on `systolic_pipe16_realbist`, 1,101 on `coproc_core`.
394 /// OpenROAD's own single-net repair clears them but groups pins, not gates: replayed on `gpu_simt`'s
395 /// post-diode placement it moved 295 of 1,907 diodes off their gate's net. Grouping whole gates cleared all
396 /// 151 signal nets there with 283 buffers and every diode still on its gate's net.
397 pub diode_fanout_repair: bool,
398 /// Loads every net is built under the SDC's `set_max_fanout` (`LOGOS_FANOUT_MARGIN`,
399 /// `scripts/openroad/logos_fanout_limit.tcl`): CTS and the diode-aware fanout step build to the limit less this,
400 /// and every report and signoff judge the limit itself.
401 ///
402 /// It is room for the one load a pass after routing can add. The post-route antenna ECO adds a diode at a pin the
403 /// checker still flags; on a net built to the limit it has nowhere to put it. MEASURED on `gpu_simt`
404 /// (RUN_2026.10.03_07.00.23, built to the limit): pin `_13114_/C`, met1 ratio 1.02, on a net of ten gates and no
405 /// diode — refused, and the block missed antenna by that one pin.
406 ///
407 /// The margin is kept by the steps that know which loads are critical, never by the SDC the resizers repair
408 /// against. MEASURED NEGATIVE, the SDC way (RUN_2026.10.03_07.23.27): `repair_design` split every ten-load net
409 /// without regard to slack, the slow-corner critical path went from 2 to 6 fanout-buffer pins, and setup slack
410 /// from +0.27 to -0.01 ns, past what a full setup repair could recover. Kept by the fanout step instead — a net at
411 /// the limit split like a diode-laden one, its least-slack gates left on the driver — the same block signed off
412 /// with every gate at zero and setup +0.19 ns (RUN_2026.10.03_07.35.09; 493 nets split, 644 buffers, 2.6% of its
413 /// nets sit at exactly the limit).
414 pub fanout_margin_loads: u32,
415 /// Run global-route timing repair without pin swapping (`LOGOS_GLB_SKIP_PIN_SWAP`). OpenROAD
416 /// 41a51eaf's post-route `repair_timing` lost one net's driver from its route tree on
417 /// `soft_core_realbist` (RSZ-0073 x51 on `_19008_/Y`, every signal net routed) and died on SIGSEGV
418 /// under both antenna recipes; the same step with `-skip_pin_swap` completes. Sizing, buffering
419 /// and hold repair are untouched.
420 pub glb_skip_pin_swap: bool,
421 /// Whether antenna is closed with diodes, or with the jog-router alone. See [`AntennaStrategy`].
422 pub antenna: AntennaStrategy,
423 /// Pin the router's thread count so a run reproduces regardless of what else the box is doing.
424 /// The divergence this prevents (a design converging 25 -> 0 DRC reaching 22,090 -> 27,653
425 /// under load) is thread oversubscription, not load itself.
426 pub routing_cores: u32,
427 /// The vertical power-strap pitch in um (`FP_PDN_VPITCH`), or OpenLane's own (153.6 on sky130) when `None`.
428 ///
429 /// A standard cell is fed along its met1 rail from the nearest vertical strap, so a cell midway between two
430 /// straps sees the longest rail, and the drop along it grows with the square of that run. MEASURED on
431 /// `fabric_horner_seq` (RUN_2026.10.03_14.54.29, anchored area sources on the strap pitch): worst 90.13 mV
432 /// against the 90 mV gate, at x 98.6-101.9 um — midway between its straps at x 21.8/25.1 and 175.4/178.7 —
433 /// while the block's average was 9.36 mV. Only the pitch shortens the rail; placement density and die size do not.
434 /// The gate is each cell's droop, its supply's drop plus its ground's rise at the cell: the same block's worst cell
435 /// drooped 106.45 mV at 76.8 um and 66.50 mV at 38.4 um (RUN_2026.10.20_17.57.25: supply 33.88 mV, ground 34.06 mV).
436 pub pdn_vpitch_um: Option<f64>,
437 /// The hold repair's slack margin in ns, for the placement and the global-route repair both
438 /// (`PL_`/`GLB_RESIZER_HOLD_SLACK_MARGIN`), or OpenLane's own (0.1 and 0.05) when `None`.
439 ///
440 /// The repair judges hold on estimated parasitics and signoff on extracted ones, so the margin must cover the
441 /// difference. MEASURED on `systolic_pipe16_realbist` (RUN_2026.10.03_18.45.27): the global-route repair met
442 /// its 0.05 ns, and signoff read -0.01, -0.03 and -0.04 ns at the fastest corner on the min, nominal and max RC
443 /// corners, after routing took the clock skew from 0.52 ns at CTS to 1.05 ns at max RC — an estimate 0.09 ns
444 /// optimistic.
445 pub hold_slack_margin_ns: Option<f64>,
446 /// The setup repair's slack margin in ns, for the placement and the global-route repair both
447 /// (`PL_`/`GLB_RESIZER_SETUP_SLACK_MARGIN`), or OpenLane's own (0.05 and 0.025) when `None`.
448 ///
449 /// The repairs judge setup on global-route estimates at the nominal RC corner and stop once those close; signoff
450 /// times the routed design extracted at the max-RC corner. MEASURED on `wpe_user_project` at 18.5 ns
451 /// (RUN_2026.10.25_06.23.41, every net routed): the repairs closed to +0.03 ns on their estimates, and the routed
452 /// design read -0.37 ns extracted at the nominal corner and -1.13 ns at the max-RC corner, at ss_100C_1v60.
453 pub setup_slack_margin_ns: Option<f64>,
454 /// The block is measured beside others (the comparative-synthesis bench), not signed off. A run whose typical
455 /// corner misses setup completes and records the slack it missed by, so a period search can read the period the
456 /// block needs; yosys's check findings (latches, out-of-range selects, undriven wires) are left to the recorder,
457 /// which records them, rather than stopping the flow. Hold stops it either way. Antenna closes with every repair
458 /// the flow has, [`ClosureSettings::eco_diode_fallback`] among them: one flow serves every candidate of a
459 /// comparison, and the bench's first attempts met timing and ended on the residual it repairs
460 /// (`qorsyn_k1_bambu_pipe_c_a7p10_p32_5`, RUN_2026.10.17_01.53.18: one met4 net at 1.02 times its limit). A
461 /// signoff block never sets it. It is synthesized for delay ([`COMPARATIVE_SYNTH_STRATEGY`]) unless its caller
462 /// names a strategy.
463 pub comparative: bool,
464}
465
466/// The synthesis script of a comparative block ([`ClosureSettings::comparative`]) whose caller names none. OpenLane
467/// hands abc the clock period as its target, and an area script recovers area down to it, so a run at a period longer
468/// than the design needs closes a netlist slower than the design: the period it closes at says more about the period
469/// it was given than about the design. A delay script maps every candidate for speed. MEASURED by the clock-push search
470/// over the signed blocks under DELAY 0 (logs/clock-push, 10-17): decision_hft_bist 15.0 -> 4.4 ns, dram_executor
471/// 10.5 -> 5.0, cgra_sumto 18 -> 6.4, rev_alu_core 14.5 -> 7.7.
472pub const COMPARATIVE_SYNTH_STRATEGY: SynthStrategy = SynthStrategy::Delay(0);
473
474/// The hold repair's margin for a comparative block ([`ClosureSettings::comparative`]) whose caller gives none. The
475/// repair judges hold at the typical corner on estimated parasitics, and signoff checks every corner on extracted ones.
476/// MEASURED on the bench's first fold-wrapped attempts: `qorsyn_k1_bambu_pipe_c_a7p10_p32_5`
477/// (RUN_2026.10.17_01.20.19) met hold by +0.09 ns at the typical corner after OpenLane's 0.05 ns repair, and missed it
478/// by 0.04, 0.05 and 0.04 ns at the fastest corner on the min, max and nominal RC corners, a 0.14 ns gap;
479/// `qorsyn_k1_bambu_fsm_c_a7p10_p20_5` missed it by 0.03 ns the same way. One margin serves every candidate of a
480/// comparison, so it covers that gap with room: 0.19 ns would close it exactly.
481pub const COMPARATIVE_HOLD_MARGIN_NS: f64 = 0.3;
482
483/// A wire limit as the flow scripts spell it: a length in um, or `openroad` for OpenROAD's own limit, emitted as 0,
484/// which OpenLane reads as unset. A bare 0 is refused, so no typo drops a limit.
485///
486/// MEASURED: `fabric_horner_seq` at 800 um died in global-route setup repair (RUN_2026.09.28_20.36.35):
487/// `rsz::makeBufferedNet` recursed without end under `RepairSetup::rebuffer` — 10,460 frames on an 8 MB stack, and
488/// with an unlimited one it grew until the kernel killed it; a cycle in one net's route tree, OpenROAD 41a51eaf. Its
489/// recorded recipe, at OpenROAD's limits, passes that step.
490pub fn parse_wire_limit_um(s: &str) -> Option<u32> {
491 if s == "openroad" {
492 return Some(0);
493 }
494 s.parse().ok().filter(|l: &u32| *l > 0)
495}
496
497impl Default for ClosureSettings {
498 fn default() -> Self {
499 Self {
500 slew_margin_pct: 30,
501 pl_slew_margin_pct: 30,
502 cap_margin_pct: 30,
503 heuristic_antenna_threshold: 300,
504 grt_antenna_margin_pct: 50,
505 grt_antenna_iters: 25,
506 diodes_before_resize: false,
507 eco_diode_fallback: false,
508 post_diode_upsize: false,
509 post_diode_upsize_margin_pct: 52,
510 post_diode_upsize_all_corners: false,
511 glb_skip_pin_swap: false,
512 cts_within_fanout: true,
513 diode_fanout_repair: true,
514 fanout_margin_loads: 1,
515 antenna_eco_iters: 30,
516 target_density: 0.45,
517 met1_route_penalty: 0.7,
518 grt_overflow_iters: None,
519 grt_adjustment: None,
520 dpl_cell_padding_per_side: None,
521 clock_max_wire_um: 800,
522 signal_max_wire_um: 800,
523 post_antenna_upsize: false,
524 antenna: AntennaStrategy::DiodesAndJog,
525 routing_cores: 4,
526 synth_strategy: None,
527 pdn_vpitch_um: None,
528 hold_slack_margin_ns: None,
529 setup_slack_margin_ns: None,
530 comparative: false,
531 }
532 }
533}
534
535/// One of OpenLane's `SYNTH_STRATEGY` scripts: `AREA 0..=3` or `DELAY 0..=4`.
536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
537pub enum SynthStrategy {
538 Area(u8),
539 Delay(u8),
540}
541
542impl SynthStrategy {
543 /// `area3` / `delay4`, or `None` for anything OpenLane does not define.
544 pub fn parse(s: &str) -> Option<SynthStrategy> {
545 let (kind, n) = s.split_at(s.find(|c: char| c.is_ascii_digit())?);
546 let n: u8 = n.parse().ok()?;
547 match kind {
548 "area" if n <= 3 => Some(SynthStrategy::Area(n)),
549 "delay" if n <= 4 => Some(SynthStrategy::Delay(n)),
550 _ => None,
551 }
552 }
553
554 /// The flag spelling [`SynthStrategy::parse`] reads back.
555 pub fn flag(&self) -> String {
556 match self {
557 SynthStrategy::Area(n) => format!("area{n}"),
558 SynthStrategy::Delay(n) => format!("delay{n}"),
559 }
560 }
561
562 /// The `SYNTH_STRATEGY` value OpenLane reads.
563 pub fn openlane(&self) -> String {
564 match self {
565 SynthStrategy::Area(n) => format!("AREA {n}"),
566 SynthStrategy::Delay(n) => format!("DELAY {n}"),
567 }
568 }
569}
570
571/// Emit the OpenLane `config.json` for a design on a process.
572///
573/// The point of generating this rather than hand-writing it: a block cannot silently be built
574/// without the settings that were measured to be necessary, and the settings cannot drift from the
575/// evidence, because both live here and a lock reads them.
576pub fn openlane_config(d: &DesignProfile, pdk: &PdkTarget, c: &ClosureSettings) -> String {
577 emit(d, pdk, c, None)
578}
579
580/// How a vehicle user macro takes power from the wrapper around it, which decides how high it may route.
581///
582/// The wrapper's grid generator keeps its straps off a macro's obstructions and feeds the macro's own power pins
583/// where the two cross. MEASURED (wrapper PDN spike, 2026-09-26: the DRAM wrapper around a copy of its macro's
584/// abstract with the met5 power ports removed and met4 obstructed across the die): every supply's met4 straps
585/// were cut at the macro — 0 of 257 segments over it, against 127 over the unmodified macro
586/// (RUN_2026.09.23_12.53.02) — and met5 crossed it and fed vccd1 and vssd1 through 190 via stacks each.
587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
588pub enum MacroGrid {
589 /// Its own grid, as a core: straps on both of the process's PDN layers, and routing below them
590 /// ([`UserArea::macro_max_layer`](super::pdk_target::UserArea::macro_max_layer)); the wrapper's grid passes
591 /// over it on both layers.
592 Own,
593 /// Straps on the vertical PDN layer only, crossed and fed by the wrapper's horizontal straps, and routing up
594 /// to that layer ([`UserArea::fed_macro_max_layer`](super::pdk_target::UserArea::fed_macro_max_layer)); the
595 /// wrapper's vertical straps stop at the macro. efabless's reference user macro.
596 WrapperFed,
597}
598
599impl MacroGrid {
600 /// `own` / `fed`.
601 pub fn parse(s: &str) -> Option<MacroGrid> {
602 match s {
603 "own" => Some(MacroGrid::Own),
604 "fed" => Some(MacroGrid::WrapperFed),
605 _ => None,
606 }
607 }
608
609 /// The flag spelling [`MacroGrid::parse`] reads back.
610 pub fn flag(&self) -> &'static str {
611 match self {
612 MacroGrid::Own => "own",
613 MacroGrid::WrapperFed => "fed",
614 }
615 }
616}
617
618/// A macro hardened to sit INSIDE a shuttle vehicle's wrapper — the Caravel user macro.
619///
620/// Its die is ours to choose, but its power nets and routing ceiling are the vehicle's: it takes the harness's
621/// supplies through power pins, and its routing ceiling follows from how the wrapper's grid feeds it
622/// ([`MacroGrid`]).
623#[derive(Debug, Clone, Copy, PartialEq)]
624pub struct VehicleMacro {
625 pub die_um: (f64, f64),
626 /// Extra hold slack the macro closes every path to. The paths the wrapper would see violated start at a
627 /// harness input port and end at a flop in here; the wrapper is a cell-free routing shell (cells in it need
628 /// taps and fill, which broke its LVS: 0 -> 7,755 with input buffering), so they are closed on THIS side —
629 /// by [`vehicle_macro_sdc`], which holds the macro's ports to the harness's real minimum input delays.
630 /// `dram_user_project` was hardened before that SDC existed and closed its 97 such paths (the pure shell
631 /// ran hold -0.57 ns over them) with a 0.7 ns margin here; a margin that size applies to every internal
632 /// path as well, which a macro with tens of thousands of flops cannot afford.
633 pub hold_margin_ns: f64,
634 pub grid: MacroGrid,
635}
636
637struct VehicleCtx<'a> {
638 m: &'a VehicleMacro,
639 power: (&'static str, &'static str),
640 max_layer: &'static str,
641}
642
643/// The config for a vehicle user macro, or a refusal when the process's vehicle has not been read.
644pub fn vehicle_macro_config(
645 d: &DesignProfile,
646 m: &VehicleMacro,
647 pdk: &PdkTarget,
648 c: &ClosureSettings,
649) -> Result<String, PdkUnsupported> {
650 let ua = pdk
651 .user_area
652 .ok_or(PdkUnsupported::VehicleUncharacterized { pdk: pdk.name })?;
653 let ceiling = match m.grid {
654 MacroGrid::Own => ua.macro_max_layer,
655 MacroGrid::WrapperFed => ua.fed_macro_max_layer,
656 };
657 match (ua.macro_power, ceiling) {
658 (Some(power), Some(max_layer)) => Ok(emit(
659 d,
660 pdk,
661 c,
662 Some(VehicleCtx {
663 m,
664 power,
665 max_layer,
666 }),
667 )),
668 _ => Err(PdkUnsupported::VehicleUncharacterized { pdk: pdk.name }),
669 }
670}
671
672/// The constraints a vehicle user macro is timed with: OpenLane's own `base.sdc`, then the vehicle's timing as it
673/// characterizes it ([`HarnessTiming`](super::pdk_target::HarnessTiming)) — the clock model the chip is signed
674/// off with, and every synchronous harness input's minimum delay.
675///
676/// The inputs: `base.sdc` gives every input one delay, `IO_PCT` of the period, as both its earliest and latest
677/// arrival — 8 ns at 40 ns — so a port-to-flop path looks hold-safe inside the macro and is violated one level
678/// up, where the harness delivers the Wishbone bus under 2 ns after the edge. With the real minimum on the ports,
679/// the macro's own hold repair closes exactly those paths, and nothing else needs a margin. MEASURED on
680/// `wpe_user_project` (RUN_2026.09.25_22.53.44): the global 0.7 ns hold margin that closed `dram_user_project`'s
681/// 97 port-to-flop paths found 35,656 hold endpoints in the engine's 32,773 flops and inserted 81,316 buffers,
682/// 0.81 mm²; the macro went from 32% to 41% utilization and global routing failed on congestion (GRT-0119).
683///
684/// The clock: `base.sdc`'s 0.25 ns uncertainty and 5 % derate are OpenLane's defaults, not the chip's; Caravel
685/// signs itself off at 0.1 ns and 3.75 %, and those are set after `base.sdc` so they are the ones in force.
686/// MEASURED on the engine's post-CTS placement (RUN_2026.09.26_03.38.30, all three corners, OpenLane's 0.1 ns
687/// repair margin): under the defaults 31,793 of its 35,747 flops fell inside the margin and took a buffer each
688/// (32,005), leaving global routing 55 gcells short; 31,089 of them were flops holding their own value through
689/// an enable mux — launch and capture on the same flop and the same edge, a path skew cannot reach — and only
690/// 87 had negative slack. Under the chip's model 6 endpoints fall inside the margin and 1 is negative: the
691/// `wbs_cyc_i` port path, a real violation the repair closes either way.
692///
693/// `clock` is the harness clock the macro's logic runs on, its `CLOCK_PORT`. On [`HarnessClock::User2`] the harness's
694/// bus keeps its own clock: `wb_clk_i` becomes a clock at the chip's core period, asynchronous to the macro's, and
695/// every port the harness times against it (`HarnessTiming::core_clock_inputs` and `core_clock_outputs`) moves from
696/// `base.sdc`'s delay against `CLOCK_PORT` to one against `wb_clk_i`. Each removal names the clock it removes:
697/// MEASURED in the pinned OpenSTA, `unset_input_delay` without `-clock` removes only the delays that have no clock,
698/// and `write_sdc` still showed `wb_clk_i` delayed against `user_clock2`.
699pub fn vehicle_macro_sdc(pdk: &PdkTarget, clock: HarnessClock) -> Result<String, PdkUnsupported> {
700 let h = pdk
701 .user_area
702 .and_then(|ua| ua.harness)
703 .ok_or(PdkUnsupported::VehicleUncharacterized { pdk: pdk.name })?;
704 let mut s = String::from("source $::env(SCRIPTS_DIR)/base.sdc\n");
705 let _ = writeln!(
706 s,
707 "set_clock_uncertainty {} [get_clocks $::env(CLOCK_PORT)]",
708 h.clock_uncertainty_ns
709 );
710 let _ = writeln!(
711 s,
712 "set_timing_derate -early [expr {{1 - {}}}]",
713 h.timing_derate
714 );
715 let _ = writeln!(
716 s,
717 "set_timing_derate -late [expr {{1 + {}}}]",
718 h.timing_derate
719 );
720 let bus_clock = match clock {
721 HarnessClock::Core => "$::env(CLOCK_PORT)",
722 HarnessClock::User2 => {
723 let (core, p) = (HarnessClock::Core.port(), h.core_clock_period_ns);
724 let off = "-clock [get_clocks $::env(CLOCK_PORT)]";
725 let on = format!("[expr {{{p} * $::env(IO_PCT)}}] -clock [get_clocks {core}]");
726 let _ = writeln!(s, "create_clock [get_ports {core}] -name {core} -period {p}");
727 let _ = writeln!(s, "set_clock_uncertainty {} [get_clocks {core}]", h.clock_uncertainty_ns);
728 let _ = writeln!(s, "set_clock_transition $::env(SYNTH_CLOCK_TRANSITION) [get_clocks {core}]");
729 let _ = writeln!(
730 s,
731 "set_clock_groups -asynchronous -group [get_clocks $::env(CLOCK_PORT)] -group [get_clocks {core}]"
732 );
733 let _ = writeln!(s, "unset_input_delay {off} [get_ports {core}]");
734 let _ = writeln!(
735 s,
736 "set_driving_cell -lib_cell $::env(SYNTH_CLK_DRIVING_CELL) -pin $::env(SYNTH_CLK_DRIVING_CELL_PIN) [get_ports {core}]"
737 );
738 for port in h.core_clock_inputs {
739 let _ = writeln!(s, "unset_input_delay {off} [get_ports {{{port}}}]");
740 let _ = writeln!(s, "set_input_delay {on} [get_ports {{{port}}}]");
741 }
742 for port in h.core_clock_outputs {
743 let _ = writeln!(s, "unset_output_delay {off} [get_ports {{{port}}}]");
744 let _ = writeln!(s, "set_output_delay {on} [get_ports {{{port}}}]");
745 }
746 core
747 }
748 };
749 for (ports, ns) in h.input_min_ns {
750 let _ = writeln!(s, "set_input_delay -min {ns} -clock [get_clocks {bus_clock}] [get_ports {{{ports}}}]");
751 }
752 Ok(s)
753}
754
755fn emit(d: &DesignProfile, pdk: &PdkTarget, c: &ClosureSettings, vm: Option<VehicleCtx>) -> String {
756 let mut s = String::new();
757 let _ = writeln!(s, "{{");
758 let _ = writeln!(s, " \"DESIGN_NAME\": \"{}\",", d.name);
759 let _ = writeln!(s, " \"VERILOG_FILES\": \"dir::src/*.v\",");
760 let _ = writeln!(s, " \"VERILOG_INCLUDE_DIRS\": \"dir::src\",");
761 let _ = writeln!(s, " \"CLOCK_PORT\": \"{}\",", d.clock_port);
762 let _ = writeln!(s, " \"CLOCK_PERIOD\": {},", d.clock_period_ns);
763
764 // The process, never assumed. A run records these into its own config.tcl, which is where the
765 // signoff recorder reads the PDK back from — so the ledger can never disagree with the build.
766 let _ = writeln!(s, " \"STD_CELL_LIBRARY\": \"{}\",", pdk.std_cell_library);
767 let top = vm.as_ref().map_or(pdk.top_routing_layer, |v| v.max_layer);
768 let _ = writeln!(s, " \"RT_MAX_LAYER\": \"{top}\",");
769
770 if let Some(st) = c.synth_strategy.or(c.comparative.then_some(COMPARATIVE_SYNTH_STRATEGY)) {
771 let _ = writeln!(s, " \"SYNTH_STRATEGY\": \"{}\",", st.openlane());
772 }
773
774 // Electrical closure.
775 let _ = writeln!(
776 s,
777 " \"PL_RESIZER_MAX_SLEW_MARGIN\": {},",
778 c.pl_slew_margin_pct
779 );
780 let _ = writeln!(
781 s,
782 " \"GLB_RESIZER_MAX_SLEW_MARGIN\": {},",
783 c.slew_margin_pct
784 );
785 let _ = writeln!(s, " \"PL_RESIZER_MAX_CAP_MARGIN\": {},", c.cap_margin_pct);
786 let _ = writeln!(s, " \"GLB_RESIZER_MAX_CAP_MARGIN\": {},", c.cap_margin_pct);
787 let _ = writeln!(s, " \"PL_RESIZER_DESIGN_OPTIMIZATIONS\": 1,");
788 let _ = writeln!(s, " \"GLB_RESIZER_DESIGN_OPTIMIZATIONS\": 1,");
789 let _ = writeln!(s, " \"PL_RESIZER_TIMING_OPTIMIZATIONS\": 1,");
790 let _ = writeln!(s, " \"GLB_RESIZER_TIMING_OPTIMIZATIONS\": 1,");
791
792 // The calibrated pre-route parasitic estimate (see `pdk_target::SKY130A_LAYER_RC`). Every
793 // resizer and pre-route STA step sizes against it; uncalibrated, it was optimistic by 1.39x
794 // (2.02x at p90) on exactly the heavy nets that then failed slew at signoff.
795 if let Some(rc) = pdk.estimator_layer_rc {
796 let table: Vec<String> = rc
797 .iter()
798 .map(|l| {
799 format!(
800 "{} {:.4e} {:.4e}",
801 l.layer, l.cap_pf_per_um, l.res_kohm_per_um
802 )
803 })
804 .collect();
805 let _ = writeln!(s, " \"LAYERS_RC\": \"{}\",", table.join(","));
806 }
807
808 // The CLOCK input port's assumed driver. OpenLane defaults `SYNTH_CLK_DRIVING_CELL` to
809 // `SYNTH_DRIVING_CELL`, so the clock port is modelled as driven by the minimum DATA-input
810 // inverter (`inv_2` on sky130). That is wrong in the pessimistic direction on the one net no
811 // resizer can fix — an input port's own transition is an assumption, not something buffering
812 // can improve — and it is measurably what remains after everything else closes:
813 // `systolic_os_pipe8_realbist` reached 3 max-slew violations and ALL THREE were that one net
814 // (`clk`, `clkbuf_0_clk/A`, and a diode on it, each at 0.97 against the 0.75 limit);
815 // `systolic_pipe16_realbist` showed the identical triple at 1.25.
816 //
817 // The cell is the PDK's OWN `ROOT_CLK_BUFFER`, read from its config.tcl and carried in
818 // `PdkTarget`. A process that has not been installed and read declares `None`, and then
819 // nothing is emitted — OpenLane keeps its default rather than being handed a guessed cell
820 // name. (It cannot be a `ref::$ROOT_CLK_BUFFER` reference: design-config references resolve
821 // before the PDK config is sourced, so the variable does not exist yet.)
822 if let Some((cell, pin)) = pdk.clk_driving_cell {
823 let _ = writeln!(s, " \"SYNTH_CLK_DRIVING_CELL\": \"{cell}\",");
824 let _ = writeln!(s, " \"SYNTH_CLK_DRIVING_CELL_PIN\": \"{pin}\",");
825 }
826
827 // Hold is a GATE, not a report: the flow refuses to emit a hold-violating block rather than
828 // producing one for a recorder to excuse. This is the setting whose absence let a wrapper ship
829 // with 97 hold-violating paths under a waiver whose stated basis was false.
830 let _ = writeln!(s, " \"QUIT_ON_HOLD_VIOLATIONS\": 1,");
831 if c.comparative {
832 let _ = writeln!(s, " \"QUIT_ON_SETUP_VIOLATIONS\": 0,");
833 let _ = writeln!(s, " \"QUIT_ON_SYNTH_CHECKS\": 0,");
834 }
835
836 // Antenna. The diode passes are a CHOICE, not a constant — see AntennaStrategy for the
837 // measurement. Emitting them unconditionally (as an earlier version of this function did)
838 // silently disabled the caller's ability to turn them off, and an experiment that believed it
839 // was running without diodes was in fact running with 106 of them.
840 // The two diode passes are emitted INDEPENDENTLY, because they are independent passes with
841 // different effects. Tying them to one flag is what made `HeuristicDiodesAndJog` unreachable.
842 let (heuristic, grt_repair) = match c.antenna {
843 AntennaStrategy::DiodesAndJog => (1, 1),
844 AntennaStrategy::JogOnly => (0, 0),
845 AntennaStrategy::HeuristicDiodesAndJog => (1, 0),
846 };
847 let _ = writeln!(s, " \"GRT_REPAIR_ANTENNAS\": {grt_repair},");
848 let _ = writeln!(s, " \"GRT_ANT_MARGIN\": {},", c.grt_antenna_margin_pct);
849 let _ = writeln!(s, " \"GRT_ANT_ITERS\": {},", c.grt_antenna_iters);
850 let _ = writeln!(
851 s,
852 " \"LOGOS_DIODES_BEFORE_RESIZE\": {},",
853 u8::from(c.diodes_before_resize)
854 );
855 let _ = writeln!(
856 s,
857 " \"LOGOS_ECO_DIODE_FALLBACK\": {},",
858 u8::from(c.eco_diode_fallback || c.comparative)
859 );
860 let _ = writeln!(
861 s,
862 " \"LOGOS_POST_DIODE_UPSIZE\": {},",
863 u8::from(c.post_diode_upsize)
864 );
865 let _ = writeln!(
866 s,
867 " \"LOGOS_POST_ANTENNA_UPSIZE\": {},",
868 u8::from(c.post_antenna_upsize)
869 );
870 let _ = writeln!(
871 s,
872 " \"LOGOS_UPSIZE_MARGIN\": {},",
873 c.post_diode_upsize_margin_pct
874 );
875 let _ = writeln!(
876 s,
877 " \"LOGOS_UPSIZE_ALL_CORNERS\": {},",
878 u8::from(c.post_diode_upsize_all_corners)
879 );
880 let _ = writeln!(
881 s,
882 " \"LOGOS_GLB_SKIP_PIN_SWAP\": {},",
883 u8::from(c.glb_skip_pin_swap)
884 );
885 // Fanout: every net within the SDC's set_max_fanout, counting the diodes the liberty counts.
886 let _ = writeln!(
887 s,
888 " \"LOGOS_CTS_FANOUT\": {},",
889 u8::from(c.cts_within_fanout)
890 );
891 let _ = writeln!(
892 s,
893 " \"LOGOS_DIODE_FANOUT\": {},",
894 u8::from(c.diode_fanout_repair)
895 );
896 let _ = writeln!(s, " \"LOGOS_FANOUT_MARGIN\": {},", c.fanout_margin_loads);
897 let _ = writeln!(s, " \"RUN_HEURISTIC_DIODE_INSERTION\": {heuristic},");
898 // A vehicle macro's ports face the wrapper's long harness nets, whose metal this flow never
899 // sees, so its inputs are protected at the pin; a standalone block's ports are not exposed that way.
900 let port_diodes = if vm.is_some() { "both" } else { "none" };
901 let _ = writeln!(s, " \"DIODE_ON_PORTS\": \"{port_diodes}\",");
902 let _ = writeln!(
903 s,
904 " \"HEURISTIC_ANTENNA_THRESHOLD\": {},",
905 c.heuristic_antenna_threshold
906 );
907 let _ = writeln!(s, " \"DRT_ANTENNA_ECO\": 1,");
908 let _ = writeln!(s, " \"ECO_JOG\": 1,");
909 let _ = writeln!(s, " \"DRT_ECO_ITERS\": {},", c.antenna_eco_iters);
910
911 // Antenna, continued: the met1 penalty is expressed through the PDK's own metal stack, so the
912 // column count can never be wrong for the process (sky130 has six columns including li1;
913 // gf180 has five and no li1 — a hand-written vector silently mismatches).
914 let _ = writeln!(
915 s,
916 " \"GRT_LAYER_ADJUSTMENTS\": \"{}\",",
917 pdk.layer_adjustments(0.99, c.met1_route_penalty)
918 );
919 if let Some(n) = c.grt_overflow_iters {
920 let _ = writeln!(s, " \"GRT_OVERFLOW_ITERS\": {n},");
921 }
922 if let Some(a) = c.grt_adjustment {
923 let _ = writeln!(s, " \"GRT_ADJUSTMENT\": {a},");
924 }
925 if let Some(n) = c.dpl_cell_padding_per_side {
926 let _ = writeln!(s, " \"DPL_CELL_PADDING\": {},", 2 * n);
927 }
928
929 // Clock tree: the wire from the clock input to the root is buffered to what the slow corner meets.
930 let _ = writeln!(s, " \"CTS_CLK_MAX_WIRE_LENGTH\": {},", c.clock_max_wire_um);
931 let _ = writeln!(
932 s,
933 " \"PL_RESIZER_MAX_WIRE_LENGTH\": {},",
934 c.signal_max_wire_um
935 );
936 let _ = writeln!(
937 s,
938 " \"GLB_RESIZER_MAX_WIRE_LENGTH\": {},",
939 c.signal_max_wire_um
940 );
941
942 // Floorplan.
943 let _ = writeln!(s, " \"PL_TARGET_DENSITY\": {},", c.target_density);
944 if let Some(p) = c.pdn_vpitch_um {
945 let _ = writeln!(s, " \"FP_PDN_VPITCH\": {p},");
946 }
947 if let Some(m) = c.hold_slack_margin_ns.or(c.comparative.then_some(COMPARATIVE_HOLD_MARGIN_NS)) {
948 let _ = writeln!(s, " \"PL_RESIZER_HOLD_SLACK_MARGIN\": {m},");
949 let _ = writeln!(s, " \"GLB_RESIZER_HOLD_SLACK_MARGIN\": {m},");
950 }
951 if let Some(m) = c.setup_slack_margin_ns {
952 let _ = writeln!(s, " \"PL_RESIZER_SETUP_SLACK_MARGIN\": {m},");
953 let _ = writeln!(s, " \"GLB_RESIZER_SETUP_SLACK_MARGIN\": {m},");
954 }
955 if d.fixed_die {
956 if let Some(ua) = pdk.user_area {
957 let _ = writeln!(s, " \"FP_SIZING\": \"absolute\",");
958 let _ = writeln!(s, " \"DIE_AREA\": \"{}\",", ua.die_area());
959 }
960 }
961 if let Some(v) = &vm {
962 let (w, h) = v.m.die_um;
963 let _ = writeln!(s, " \"FP_SIZING\": \"absolute\",");
964 let _ = writeln!(s, " \"DIE_AREA\": \"0 0 {w} {h}\",");
965 let _ = writeln!(s, " \"VDD_NETS\": [\"{}\"],", v.power.0);
966 let _ = writeln!(s, " \"GND_NETS\": [\"{}\"],", v.power.1);
967 let _ = writeln!(s, " \"SYNTH_USE_PG_PINS_DEFINES\": \"USE_POWER_PINS\",");
968 if v.m.grid == MacroGrid::WrapperFed {
969 let _ = writeln!(s, " \"DESIGN_IS_CORE\": 0,");
970 }
971 let _ = writeln!(s, " \"FP_PDN_ENABLE_RAILS\": 1,");
972 // The pin order MUST mirror the wrapper DEF, or the wrapper's Wishbone nets route ~2.4 mm
973 // to reach the pins and come back as antenna violators (scripts/gen-caravel-macro-pinorder.py).
974 let _ = writeln!(s, " \"FP_PIN_ORDER_CFG\": \"dir::pin_order.cfg\",");
975 let _ = writeln!(s, " \"RT_CLOCK_MIN_LAYER\": \"met2\",");
976 if pdk.user_area.is_some_and(|ua| ua.harness.is_some()) {
977 let _ = writeln!(s, " \"BASE_SDC_FILE\": \"dir::vehicle_macro.sdc\",");
978 }
979 let _ = writeln!(
980 s,
981 " \"PL_RESIZER_HOLD_SLACK_MARGIN\": {},",
982 v.m.hold_margin_ns
983 );
984 let _ = writeln!(
985 s,
986 " \"GLB_RESIZER_HOLD_SLACK_MARGIN\": {},",
987 v.m.hold_margin_ns
988 );
989 let _ = writeln!(s, " \"MAGIC_WRITE_FULL_LEF\": 0,");
990 let _ = writeln!(s, " \"RUN_CVC\": 0,");
991 }
992 if !d.macros.is_empty() {
993 let lefs: Vec<String> = d
994 .macros
995 .iter()
996 .map(|m| format!("dir::macro/{m}.lef"))
997 .collect();
998 let _ = writeln!(s, " \"EXTRA_LEFS\": \"{}\",", lefs.join(" "));
999 }
1000
1001 // Reproducibility, last so it reads as the closing invariant it is.
1002 let _ = writeln!(s, " \"ROUTING_CORES\": {}", c.routing_cores);
1003 let _ = writeln!(s, "}}");
1004 s
1005}