Skip to main content

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}