Low-level build + (future) FFI bindings for OpenROAD's OpenDB (libodb) — built
standalone from a pinned, sparse OpenROAD subtree, with no Tcl, no SWIG, and no
OpenROAD engines. The safe Rust API lives in the sibling crate vyges-opendb.
Part of Vyges Loom.
libodbis the in-memory design database every OpenROAD engine reads and writes; binding it lets Loom do ECO/audit/extraction natively over.odb— and it carries the LEF/DEF/GDS/CDL I/O with it.
A single static libodb.a (the OpenDB core + the utl logger), buildable on
linux/x86_64, linux/arm64, and macOS/Apple Silicon. Verified: it reads a real placed +
routed .odb, walks the model, and writes it back — linking none of the engines.
openroad-pin.yamlpins the OpenROAD commit (matches thevyges-opendbdistribution).scripts/fetch-odb-src.shdoes a blobless, cone-sparse checkout of onlysrc/odb+src/utl+cmakeat that SHA — ~24 MB, not the ~1.8 GB full tree.CMakeLists.txtcompiles the db core + utl intolibodb.a(C++20), linking Boost (headers), zlib, spdlog, fmt, abseil. Tcl/SWIG/or-tools/engines are deliberately excluded..github/workflows/build-libodb.ymlbuilds + smoke-tests + publisheslibodb.aper-arch, on demand (workflow_dispatch).
- Prebuilt (fast, no toolchain-heavy build): download the per-arch bundle the
build-libodbworkflow publishes (lib/libodb.a+include/{odb,utl}+ the OpenROAD license), extract it, and setVYGES_ODB_PREBUILT_DIR=<dir>.build.rslinks the archive directly — no cmake, no OpenROAD fetch, nolibodbcompile. This is the path for consumers (e.g.vyges-cli) and CI. - From source (default):
scripts/fetch-odb-src.sh+ CMake, below.
scripts/fetch-odb-src.sh # sparse-checkout the pinned subtree -> vendor/OpenROAD
cmake -S . -B build -DVYGES_ODB_SMOKE=ON
cmake --build build -j
./build/odb_smoke test/fixtures/counter.odb /tmp/rt.odb # -> block=counter insts=229 ...Deps: a C++20 compiler + cmake boost zlib abseil spdlog fmt (apt lib*-dev, or
brew install).
- v0 (now): the db core — the in-memory model +
.odbread/write (dbDatabase, the ECO journal, wire codec, RC, connectivity). Enough for the odb applier + audit steps. - v1 (next): add the LEF/DEF/GDS/CDL I/O sub-libs (
defin/lefin/gdsin/...). - bindings (next): the
cxxbridge (this crate'ssrc/) + the safevyges-opendbAPI.
The pinned ODB carries the 3D chiplet schema — one design is multiple dies bonded together,
each with its own dbTech. These classes are in db.h and compile into libodb.a like any
other, so they need no special build. Bound so far:
| Folded | Keyed by |
|---|---|
dbChip |
chip name |
dbChipInst |
parent chip, inst |
dbChipRegion |
chip, region |
dbChipRegionInst |
chip, inst, region |
dbChipBump |
chip, region, index |
dbChipConn / dbChipNet / dbChipPath |
chip, name |
| Unfolded (derived) | Keyed by |
|---|---|
dbUnfoldedChipInst |
slash-joined chip-inst path |
dbUnfoldedChipRegionInst |
path, index |
dbUnfoldedChipBumpInst |
path, region index, index |
dbUnfoldedChipConn / dbUnfoldedChipNet |
index |
dbChipBumpInst is deliberately not bound: every accessor it has returns an unnameable
type, so it would contribute no fields. The same information is reachable through dbChipBump
(folded) or dbUnfoldedChipBumpInst (absolute positions).
odb's own 3D checker is built in and reachable as check_3dblox. It covers logical
connectivity, floating chips, overlapping dies, unused INTERNAL_EXT regions, connection-region
overlap and mating-surface gap versus connection thickness, bump physical alignment, and
alignment markers.
It reports the way odb reports every other violation — as dbMarker objects under a 3DBlox
category on the top chip, one sub-category per check — so findings are read back through the
ordinary marker accessors, no new read path required. It also logs each finding through
utl::Logger.
One consequence worth knowing: utl::Logger's default sink writes to stdout, so a caller
emitting machine-readable output on stdout will have it corrupted. log_capture_begin /
log_capture_end wrap a call with the diagnostics captured to a string instead, leaving stdout
to the caller. Capture detaches every sink for the duration (including the events forwarder) and
restores them afterwards, so messages emitted while captured reach the events trail only through
what the caller does with the returned text.
Only checker.cpp is compiled from src/3dblox. The rest of that directory links
yaml-cpp (the .3dbv/.3dbx parsers) and OpenSTA (3dblox.cpp's Verilog/Liberty path),
neither of which belongs in an engine-free libodb. checker.cpp itself needs nothing beyond
odb + utl + spdlog/fmt + Boost geometry, all already linked.
The entry point is src/lint3d.{h,cpp}, which is ours and is the one file of ours compiled
by CMakeLists rather than by the cxx build. checker.h is an internal odb header, not part of
the public include/ tree, so a shim that reached into src/3dblox would break the
prebuilt-bundle path (which ships only include/{odb,utl}) — and constructing odb::Checker
directly would put its object layout in the caller's stack frame. Keeping it inside libodb
avoids both.
Four more things here will trip you up if they are not written down.
odb ships no getString() for dbChip::ChipType, so the mapping to text is the caller's
problem. odb solves it for itself three separate times — in 3dblox/dbvWriter.cpp, in the
3Dblox parser, and in its own Python SWIG typemap (swig/python/dbenums.i) — so generating
one here is the sanctioned pattern, not a workaround. ENUM_MAPPED in
scripts/generate-bindings.py handles this generically: any getString()-less enum is one
table entry plus a generated helper.
The vocabulary we emit is UPPERCASE — DIE, RDL, IP, SUBSTRATE, HIER — matching
both our other enums (dbSigType → "SIGNAL") and OpenROAD's own Python bindings.
Note that the 3Dblox file format spells these lowercase (die, rdl, ip, substrate,
hier). That is the .3dbv writer's representation of the value, not the database API's, and
this is a database binding — so the two disagree on purpose. Anything that later reads or
writes 3Dblox files will need the lowercase form, in the other direction. Do not "fix" one
to match the other.
The generated helper is an if-chain rather than a switch: an unrecognised value falls through
to "" instead of tripping -Wswitch. An OpenROAD pin bump can add enumerators, and a blank
is safer than silently reporting the wrong type.
setLoc does not store the point you give it. It orients the master chip's cuboid, then
stores the delta that lands that cuboid's lower-left-lower corner on your point. getLoc()
is getCuboid().lll(), which re-applies the current orientation.
So setLoc and getLoc are not the symmetric pair the names suggest, and:
inst->setLoc(Point3D(1000, 2000, 3000));
inst->setOrient(dbOrientType3D(dbOrientType::R90, true)); // WRONG ORDER
// getLoc() now reads (-39000, 2000, 2300) — the chip moved, with no error or warning
inst->setOrient(dbOrientType3D(dbOrientType::R90, true));
inst->setLoc(Point3D(1000, 2000, 3000)); // correct
// getLoc() reads (1000, 2000, 3000)Both setters now ship, and the safe API composes them in the right order. STRUCT_IN
expands a geometry struct param into its constructor's scalar components — the mirror of
STRUCT_FIELDS on the read side — so setLoc(const Point3D&) becomes
set_loc(x, y, z) (and dbChip::setOffset(const Point&) becomes set_offset(x, y)).
Before that, setLoc was unmarshallable and only setOrient could be emitted — the
destructive half of the pair, with no way for a caller to put the chip back. dbChipInst
therefore exposed no setters at all. That restriction is now lifted.
Prefer Db::place_chip_inst(chip, inst, orient, x, y, z) in the sibling crate over calling
the two setters yourself: it orients then places, so the location reads back exactly as passed.
tests/generated_write.rs asserts both that guarantee and that the wrong order really does move
the chip — if the latter ever stops failing, odb changed and the ordering needs revisiting.
A second, unrelated ordering trap. dbChipInst::create walks the master chip's regions and
bumps and derives the matching dbChipRegionInst / dbChipBumpInst there and then. Regions
added to the master afterwards are simply not instantiated for that inst — silently, with no
error. Build a chip's surfaces first, then instantiate it.
dbUnfolded* is constructUnfoldedModel()'s output — the hierarchy flattened to absolute
positions, which is what linting, the 3D viewer and full-chip analysis consume. It is never
serialised. You do not have to call anything, though: _dbDatabase::operator>> runs
constructUnfoldedModel() on read whenever the database holds more than one chip, so the
unfolded accessors answer straight after a plain open.
The one prerequisite is that the database's top chip is the assembly. dbUnfoldedBuilder
starts from dbDatabase::getChip() and walks its chip insts, so if the top chip is still some
flat design that has no chip insts, every unfolded table comes back empty and nothing tells you
why.
- C++20 required — odb headers use
operator<=>and<numbers>. utlbundles a self-contained Prometheus metrics server (Boost.Asio + std only) thatLogger.cppconstructs unconditionally, so it is compiled in; it pulls no external prometheus-cpp.- OpenROAD is BSD-3-Clause; this repo (our CMake, scripts, workflow, bindings) is Apache-2.0.