Detailed placement over the OpenDB design database: legality checking and legalization.
vyges physical dpl check-placement design.odb
vyges physical dpl detailed-placement design.odb --out-odb out.odb
detailed-placement takes upstream's own tunables — --max-displacement,
--site-search-window, --row-search-window, --drc-penalty,
--disable-window-extension, --use-diamond-legalizer — with upstream's defaults. Run
--help for the full list, and --describe for the machine-readable contract.
ℹ️ Two of upstream's options are deliberately absent. -disallow_one_site_gaps is deprecated
there (it warns DPL-3 and ignores the flag, because the setting is derived from
hasOneSiteMaster()), so accepting it would promise a control that cannot change the answer;
this engine refuses it with that explanation rather than accepting it silently. -incremental
is unimplemented and named in not_done.
⛔ Every run names what it did NOT do. check-placement reports not_checked (families it
did not evaluate) and limitations (families it did evaluate, but could not see everything);
detailed-placement reports not_done. A clean verdict from a partial tool must not read as a
complete one, so those fields are emitted whether or not they are empty.
Two legalizers, and the default is the one upstream defaults to: negotiated congestion, in
which cells are allowed to overlap, contested sites accumulate a history cost, and each iteration
rips up and re-places until nobody overlaps. --use-diamond-legalizer selects the other — a
diamond search outward from each cell's own position — mirroring upstream's flag of the same name.
⬜ Not implemented, and named in not_done on every run: groups and regions, placement padding
values, incremental placement, and two of countDRCViolations' four terms — checkEdgeSpacing
(needs each master's LEF58 cell-edge list) and checkBlockedLayers.
checkPadding was in exactly that
position until it was built, and wiring it moved aes by 5,700 cells.
Every run also names, in filtered_out, each instance the model filter excluded — a filter that
drops instances silently is indistinguishable from a design that has none of them.
Seven of upstream's nine check families are evaluated: site alignment, placed, overlap, in rows, padding, blocked layers and one-site gaps.
| family | status |
|---|---|
| site alignment · placed · overlap · in rows | ✅ evaluated |
| padding | ✅ evaluated, with zero padding — no set_placement_padding model, so only class-pair conflicts are caught |
| blocked layers | ✅ evaluated where the design has a vertical M2/M3 special wire; where it has none the mask is empty and a pass would be vacuous, so it is reported as unchecked instead |
| one-site gaps | ✅ evaluated when the technology has no one-site master — the condition upstream derives it from, not a user option |
| region placement | ⬜ needs a region model |
| edge spacing | ⬜ the LEF58 cell-edge rule is implemented and tested; what is missing is each master's edge list |
🔑 The checker came first on purpose. It is the oracle — it needs no legalizer to be correct in
order to be useful — and upstream's own suite leans on it harder: 77 of 92 .tcl tests call
check_placement, 68 call detailed_placement.
✅ Legalization matches the reference on every comparable case in its own regression suite —
28 of 28, at pin 7d490b8ecd357199c0c0e9f3e32becd5eb507c34.
🔑 The agreement is SWEEP-LEVEL, not final-placement only. Upstream's own per-iteration debug
trace and this engine's match line for line — same cell, same order, same chosen position, every
iteration — on simple05, simple07, gcd (574 lines) and hybrid_cells.
ℹ️ It read 18 of 28 when the pin first moved from 945a9f48dc6e5cc91d865daa92c45a1094cb682c,
because upstream reworked the negotiation legalizer's initial snapping across seven commits and
refreshed 24 of its own goldens. Four mechanisms recovered it:
| mechanism | what it changed |
|---|---|
Grid::gridRoundX |
the initial x rounds to the nearest site rather than truncating |
displacementInSites / rowDispInSites |
displacement is site widths on both axes, so a row jump is no longer priced as one step — and the search wavefront reorders with it |
initialSnap()'s diamond search |
replaces a four-direction scan that could not see a diagonal |
| the snap runs as its own pass | after every fixed cell is blockaded, testing grid capacity rather than merely whether a site exists |
The three large designs, at the current pin:
| design | components | result at 7d490b8 |
|---|---|---|
aes |
21,340 | identical |
ibex |
34,184 | identical |
gcd |
549 | identical |
| the other 25 cases | — | identical |
.tcl cases calling
detailed_placement, not 63: five (report_failures, fragmented_row03, pad02, fillers8,
obstruction2) wrap it as catch { detailed_placement }, so the harness's command filter does not
see them and they are scored by nothing. All five are error-path cases.
🔑 The agreement is sweep-level, not final-placement only. Upstream's own per-iteration debug trace and this engine's match line for line — same cell, same order, same chosen position, every iteration. A matching output can be coincidence; a matching decision sequence is the algorithm.
⛔ That is a claim about what this corpus asks, not about every design. 35 of upstream's 63
detailed_placement cases are outside it and are not scored at all: 12 ship no golden, 8 need
filler placement, 7 declare regions or groups, 7 need placement padding values, 1 needs both.
Correlated against OpenROAD at pin 945a9f48dc6e5cc91d865daa92c45a1094cb682c, in both
directions — a checker that cannot fail proves nothing:
| design | reference | this engine |
|---|---|---|
aes.defok (legal) |
clean | 21,340 cells, 0 violations |
cell_on_block1.def (illegal) |
"Site aligned check failed (4)" | 4 site-align failures |
Four behaviours that are easy to get wrong and are pinned as tests at their sites:
- Site alignment is core-relative. Upstream compares
cell->getLeft() % siteWidth, andgetLeft()is relative tocore_.xMin(). Read as an absolute coordinate it reports every cell of a clean design as misaligned — measured, 21,340 of 21,340. - A site-alignment failure removes the cell from the overlap comparison entirely. That is a
side effect of upstream's
continue, not a separate rule:checkOverlapis what paints a cell into its pixels, so a skipped cell is never there to collide with. - A cell's height in ROWS comes from its master, not from where the cell currently sits. A cell resting a few database units above its row covers two row bands; it is still a single-height cell, and every rule that treats multi-row cells specially depends on the difference.
- A hard placement blockage reaches the legality test only through pixel CAPACITY. Nothing in the earlier guards — in-die, valid row, site orientation — consults pixel validity, so a legality test that stops before the final footprint loop calls a cell inside a blockage perfectly legal.
ℹ️ The overlap acceleration differs deliberately — a rectangle sweep here, a pixel walk upstream. The predicate is identical and the failing set matches; which partner is reported can differ, because upstream names whichever cell already owns the pixel.
--describe reports maturity: partial, and a test fails if that word is left behind when the
unimplemented families listed above are filled in.
not_done, not_checked and
limitations in the run output are the machine-readable version of it, and they are what to trust
over any prose here.
Apache-2.0. See LICENSE and NOTICE.
ℹ️ This engine reads the design database through vyges-opendb, which binds
OpenROAD's OpenDB (libodb) — BSD 3-Clause, Copyright (c) 2019-2026 The Regents of the
University of California. The attribution is in NOTICE.