Skip to content

Repository files navigation

vyges-opendb-lib

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. libodb is 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.

What this repo produces

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.

How it works — pinned + sparse, no full mirror

  • openroad-pin.yaml pins the OpenROAD commit (matches the vyges-opendb distribution).
  • scripts/fetch-odb-src.sh does a blobless, cone-sparse checkout of only src/odb + src/utl + cmake at that SHA — ~24 MB, not the ~1.8 GB full tree.
  • CMakeLists.txt compiles the db core + utl into libodb.a (C++20), linking Boost (headers), zlib, spdlog, fmt, abseil. Tcl/SWIG/or-tools/engines are deliberately excluded.
  • .github/workflows/build-libodb.yml builds + smoke-tests + publishes libodb.a per-arch, on demand (workflow_dispatch).

Two ways to get libodb

  • Prebuilt (fast, no toolchain-heavy build): download the per-arch bundle the build-libodb workflow publishes (lib/libodb.a + include/{odb,utl} + the OpenROAD license), extract it, and set VYGES_ODB_PREBUILT_DIR=<dir>. build.rs links the archive directly — no cmake, no OpenROAD fetch, no libodb compile. This is the path for consumers (e.g. vyges-cli) and CI.
  • From source (default): scripts/fetch-odb-src.sh + CMake, below.

Build locally

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).

Scope

  • v0 (now): the db core — the in-memory model + .odb read/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 cxx bridge (this crate's src/) + the safe vyges-opendb API.

3D / chiplet (ODB 3D-IC)

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).

check_3dblox — the 3D structural linter

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.

dbChip::ChipType — we generate the string mapping, and it is UPPERCASE

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.

dbChipInst::setLoc is orientation-dependent — set the orientation FIRST

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.

Regions and bumps must exist before the chip inst that uses them

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.

The unfolded model is derived, but it is rebuilt for you on read

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.

Notes

  • C++20 required — odb headers use operator<=> and <numbers>.
  • utl bundles a self-contained Prometheus metrics server (Boost.Asio + std only) that Logger.cpp constructs 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.

About

Standalone build + Rust FFI for OpenROAD OpenDB (libodb) — pinned sparse subtree, no tcl/swig/engines; x86_64/arm64/Apple Silicon

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages