From 5fadd7df16c3dda9667352329195cc1d2c371536 Mon Sep 17 00:00:00 2001 From: "Ahmed R. Mohamed" Date: Wed, 11 Mar 2026 06:36:11 +0000 Subject: [PATCH] odb: impl 3dblox parser path asserstions Signed-off-by: Ahmed R. Mohamed --- src/odb/include/odb/3dblox.h | 1 - src/odb/include/odb/db.h | 4 + src/odb/include/odb/unfoldedModel.h | 5 + src/odb/src/3dblox/3dblox.cpp | 140 ++++---- src/odb/src/3dblox/CMakeLists.txt | 1 + src/odb/src/3dblox/checker.cpp | 300 +++++++++++++++++- src/odb/src/3dblox/checker.h | 4 +- src/odb/src/3dblox/dbxParser.cpp | 5 +- src/odb/src/3dblox/routingGraph.cpp | 133 ++++++++ src/odb/src/3dblox/routingGraph.h | 33 ++ .../src/codeGenerator/schema/chip/dbChip.json | 6 + src/odb/src/db/dbChip.cpp | 21 ++ src/odb/src/db/dbChip.h | 1 + src/odb/src/db/dbDatabase.h | 6 +- src/odb/src/db/unfoldedModel.cpp | 1 + src/odb/test/cpp/BUILD | 1 + src/odb/test/cpp/CMakeLists.txt | 2 +- src/odb/test/cpp/Test3DBloxCheckerFixture.h | 6 +- .../cpp/Test3DBloxCheckerPathAssertions.cpp | 180 +++++++++++ 19 files changed, 785 insertions(+), 65 deletions(-) create mode 100644 src/odb/src/3dblox/routingGraph.cpp create mode 100644 src/odb/src/3dblox/routingGraph.h create mode 100644 src/odb/test/cpp/Test3DBloxCheckerPathAssertions.cpp diff --git a/src/odb/include/odb/3dblox.h b/src/odb/include/odb/3dblox.h index b6e07ff5626..98d9f09ac4c 100644 --- a/src/odb/include/odb/3dblox.h +++ b/src/odb/include/odb/3dblox.h @@ -33,7 +33,6 @@ struct Connection; struct DesignDef; struct BumpMapEntry; struct DbxData; - class ThreeDBlox { public: diff --git a/src/odb/include/odb/db.h b/src/odb/include/odb/db.h index 279f92828f1..935fcb81d9c 100644 --- a/src/odb/include/odb/db.h +++ b/src/odb/include/odb/db.h @@ -7262,6 +7262,10 @@ class dbChip : public dbObject bool isTsv() const; + void setBlackbox(bool blackbox); + + bool isBlackbox() const; + dbSet getChipRegions() const; dbSet getMarkerCategories() const; diff --git a/src/odb/include/odb/unfoldedModel.h b/src/odb/include/odb/unfoldedModel.h index 7504eb0d2f2..0c3c8c04d74 100644 --- a/src/odb/include/odb/unfoldedModel.h +++ b/src/odb/include/odb/unfoldedModel.h @@ -84,6 +84,11 @@ struct UnfoldedChip Cuboid cuboid; dbTransform transform; + // True when the master chip has no detailed routing (blackbox stage). + // The routing graph adds a complete clique across all regions of a blackbox + // chip so they are treated as mutually reachable. + bool is_blackbox = false; + std::deque regions; std::unordered_map region_map; diff --git a/src/odb/src/3dblox/3dblox.cpp b/src/odb/src/3dblox/3dblox.cpp index 5c10e56a4f6..a6f5271ac7a 100644 --- a/src/odb/src/3dblox/3dblox.cpp +++ b/src/odb/src/3dblox/3dblox.cpp @@ -200,6 +200,61 @@ void ThreeDBlox::readDbx(const std::string& dbx_file) } } +static std::vector splitPath(const std::string& path) +{ + std::vector parts; + std::istringstream stream(path); + std::string part; + + while (std::getline(stream, part, '/')) { + if (!part.empty()) { + parts.push_back(part); + } + } + + return parts; +} + +// Resolves a path string of the form "inst1/inst2.regions.regionName" into a +// dbChipRegionInst* by walking the chip hierarchy starting from root_chip. +// If path_insts is non-null, each visited dbChipInst* is appended to it. +// Returns nullptr if any component of the path is not found; does not log. +static dbChipRegionInst* walkRegionPath(const std::string& path, + dbChip* root_chip, + std::vector* path_insts) +{ + auto path_parts = splitPath(path); + if (path_parts.empty()) { + return nullptr; + } + const size_t regions_pos = path_parts.back().find(".regions."); + if (regions_pos == std::string::npos) { + return nullptr; + } + const std::string region_name = path_parts.back().substr(regions_pos + 9); + path_parts.back() = path_parts.back().substr(0, regions_pos); + + if (path_insts) { + path_insts->reserve(path_parts.size()); + } + dbChip* curr_chip = root_chip; + dbChipInst* curr_inst = nullptr; + for (const auto& inst_name : path_parts) { + curr_inst = curr_chip->findChipInst(inst_name); + if (!curr_inst) { + return nullptr; + } + if (path_insts) { + path_insts->push_back(curr_inst); + } + curr_chip = curr_inst->getMasterChip(); + } + if (!curr_inst) { + return nullptr; + } + return curr_inst->findChipRegionInst(region_name); +} + void ThreeDBlox::check() { Checker checker(logger_, db_); @@ -503,6 +558,7 @@ void ThreeDBlox::createChiplet(const ChipletDef& chiplet) if (chip->getChipType() != dbChip::ChipType::HIER && chip->getBlock() == nullptr) { // blackbox stage, create block + chip->setBlackbox(true); auto block = odb::dbBlock::create(chip, chiplet.name.c_str()); const int x_min = chip->getScribeLineWest() + chip->getSealRingWest(); const int y_min = chip->getScribeLineSouth() + chip->getSealRingSouth(); @@ -698,67 +754,41 @@ void ThreeDBlox::createChipInst(const ChipletInst& chip_inst) static_cast(std::round(chip_inst.z * dbu_per_micron)), }); } -static std::vector splitPath(const std::string& path) -{ - std::vector parts; - std::istringstream stream(path); - std::string part; - - while (std::getline(stream, part, '/')) { - if (!part.empty()) { - parts.push_back(part); - } - } - - return parts; -} - dbChipRegionInst* ThreeDBlox::resolvePath(const std::string& path, std::vector& path_insts) { - if (path == "~") { + if (path == "~" || path == "null") { return nullptr; } - // Split the path by '/' - std::vector path_parts = splitPath(path); - - if (path_parts.empty()) { - logger_->error(utl::ODB, 524, "3DBX Parser Error: Invalid path {}", path); - } - - // The last part should contain ".regions.regionName" - std::string last_part = path_parts.back(); - size_t regions_pos = last_part.find(".regions."); - if (regions_pos == std::string::npos) { - return nullptr; // Invalid format - } - - // Extract chip instance name and region name from last part - std::string last_chip_inst = last_part.substr(0, regions_pos); - std::string region_name = last_part.substr(regions_pos + 9); - - // Replace the last part with just the chip instance name - path_parts.back() = last_chip_inst; - - // Traverse hierarchy and find region - path_insts.reserve(path_parts.size()); - dbChip* curr_chip = db_->getChip(); - dbChipInst* curr_chip_inst = nullptr; - - for (const auto& inst_name : path_parts) { - curr_chip_inst = curr_chip->findChipInst(inst_name); - if (curr_chip_inst == nullptr) { - logger_->error(utl::ODB, - 522, - "3DBX Parser Error: Chip instance {} not found in path {}", - inst_name, - path); + auto* region = walkRegionPath(path, db_->getChip(), &path_insts); + if (!region) { + // Determine which component failed for a specific error message. + // Re-walk without collecting path_insts to find the failing step. + auto path_parts = splitPath(path); + if (path_parts.empty()) { + logger_->error(utl::ODB, 524, "3DBX Parser Error: Invalid path {}", path); + } + // Check if the path has ".regions." at all + const std::string& last = path_parts.back(); + size_t rpos = last.find(".regions."); + if (rpos == std::string::npos) { + return nullptr; // Invalid format + } + std::string region_name = last.substr(rpos + 9); + path_parts.back() = last.substr(0, rpos); + + dbChip* curr_chip = db_->getChip(); + for (const auto& inst_name : path_parts) { + if (!curr_chip->findChipInst(inst_name)) { + logger_->error( + utl::ODB, + 522, + "3DBX Parser Error: Chip instance {} not found in path {}", + inst_name, + path); + } + curr_chip = curr_chip->findChipInst(inst_name)->getMasterChip(); } - path_insts.push_back(curr_chip_inst); - curr_chip = curr_chip_inst->getMasterChip(); - } - auto region = curr_chip_inst->findChipRegionInst(region_name); - if (region == nullptr) { logger_->error(utl::ODB, 523, "3DBX Parser Error: Chip region {} not found in path {}", diff --git a/src/odb/src/3dblox/CMakeLists.txt b/src/odb/src/3dblox/CMakeLists.txt index dde022f4aa3..3ba38ea4c39 100644 --- a/src/odb/src/3dblox/CMakeLists.txt +++ b/src/odb/src/3dblox/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(3dblox dbxWriter.cpp verilogWriter.cpp 3dblox.cpp + routingGraph.cpp checker.cpp ) diff --git a/src/odb/src/3dblox/checker.cpp b/src/odb/src/3dblox/checker.cpp index 22067fd2633..8bdddcbb6cd 100644 --- a/src/odb/src/3dblox/checker.cpp +++ b/src/odb/src/3dblox/checker.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -17,6 +19,7 @@ #include "odb/dbObject.h" #include "odb/geom.h" #include "odb/unfoldedModel.h" +#include "routingGraph.h" #include "utl/Logger.h" #include "utl/unionFind.h" @@ -89,6 +92,192 @@ bool isValid(const UnfoldedConnection& conn) return (surfaces.top_z - surfaces.bot_z) == conn.connection->getThickness(); } +// --------------------------------------------------------------------------- +// BfsState +// +// Scratch buffers for assertNetRoutability, allocated once per +// checkPathAssertions pass and reused across every net call. +// +// Dirty-index tracking avoids O(n) std::fill resets between calls: +// is_blocked / is_target: we record which indices were set and clear only +// those bits in reset() — O(|dirty|) instead of O(n). +// visited: every visited node was enqueued, so the queue itself serves as +// the dirty list; reset() walks queue[] to clear visited[]. +// --------------------------------------------------------------------------- +struct BfsState +{ + std::vector is_blocked; + std::vector is_target; + std::vector visited; + std::vector queue; + std::vector dirty_blocked; + std::vector dirty_target; + + explicit BfsState(uint32_t n) + : is_blocked(n, 0), is_target(n, 0), visited(n, 0) + { + queue.reserve(std::min(n, 4096u)); + dirty_blocked.reserve(64); + dirty_target.reserve(64); + } + + // Restore all bitmasks to all-false and empty the queue. + // Called automatically by the Guard in assertNetRoutability; do not call + // directly. + void reset() + { + for (const uint32_t id : dirty_blocked) { + is_blocked[id] = 0; + } + for (const uint32_t id : dirty_target) { + is_target[id] = 0; + } + for (const uint32_t id : queue) { + visited[id] = 0; + } + dirty_blocked.clear(); + dirty_target.clear(); + queue.clear(); + } +}; + +// --------------------------------------------------------------------------- +// assertNetRoutability +// +// Core BFS-based path-assertion validator. Returns true iff every node in +// must_touch_ids lies in the same connected component of the graph after +// masking out every node in do_not_touch_ids. +// +// Algorithmic rationale +// --------------------- +// Because Steiner-tree branching and revisiting are both allowed, and because +// we validate only one net at a time (no resource competition), physical +// routability reduces strictly to a connected-components query. A single BFS +// from any must_touch seed is sufficient and optimal — this is NOT a TSP or +// Hamiltonian-path problem. +// +// Complexity: O(V + E) time per call; O(V) auxiliary space allocated once +// in BfsState and reused across all calls within a pass. +// +// Memory layout notes +// ------------------- +// is_blocked / is_target / visited are std::vector (1 byte/element). +// For 1M nodes each bitmask occupies ~1 MB; all three fit comfortably +// inside a typical 6–12 MB L3 cache, keeping BFS accesses fast. +// +// The BFS queue is a std::vector walked by a head index — giving +// sequential, hardware-prefetchable access without deque pointer-chasing. +// +// CSR adjacency: neighbors of u are g.neighbors[g.offsets[u]..g.offsets[u+1]) +// — a single contiguous slice with no per-node heap indirection. +// +// Early exit: `remaining` decrements on each new must_touch discovery; +// returns immediately when it hits zero. +// --------------------------------------------------------------------------- +// Returns an empty vector when all must_touch regions are mutually reachable +// (success), or a non-empty vector of the unreachable must_touch IDs +// (violation). For early hard failures (out-of-range or blocked IDs) only the +// failing must_touch ID is returned. +std::vector assertNetRoutability( + const RoutingGraph& g, + const std::vector& must_touch_ids, + const std::vector& do_not_touch_ids, + BfsState& s) +{ + // RAII guard: s.reset() is called on every exit path — early returns, + // normal returns, and exceptions — without any manual bookkeeping. + struct Guard + { + BfsState& state; + ~Guard() { state.reset(); } + } guard{s}; + + if (must_touch_ids.empty()) { + return {}; // No waypoints → trivially satisfiable. + } + + const uint32_t n = g.num_nodes; + + // Populate the blocked mask (do_not_touch walls). + for (const uint32_t id : do_not_touch_ids) { + if (id < n && !s.is_blocked[id]) { + s.is_blocked[id] = 1; + s.dirty_blocked.push_back(id); + } + } + + // Populate the target mask and choose the BFS seed. + // If any must_touch node is also blocked the assertion is unsatisfiable. + uint32_t remaining = 0; + uint32_t start = std::numeric_limits::max(); + + for (const uint32_t id : must_touch_ids) { + if (id >= n) { + // Out-of-range: all waypoints are unreachable. + return {id}; + } + if (s.is_blocked[id]) { + // Required waypoint is walled off: assertion cannot be satisfied. + return {id}; + } + if (!s.is_target[id]) { // De-duplicate repeated entries. + s.is_target[id] = 1; + s.dirty_target.push_back(id); + ++remaining; + } + if (start == std::numeric_limits::max()) { + start = id; // Seed BFS from the first valid must_touch. + } + } + + // BFS on the masked graph ------------------------------------------------- + + s.visited[start] = 1; + s.queue.push_back(start); + + if (s.is_target[start] && --remaining == 0) { + return {}; // Degenerate: single unique must_touch node, already found. + } + + // Walk the queue by index — sequential access is hardware-prefetchable. + for (size_t head = 0; head < s.queue.size(); ++head) { + const uint32_t u = s.queue[head]; + for (uint32_t i = g.offsets[u]; i < g.offsets[u + 1]; ++i) { + const uint32_t v = g.neighbors[i]; + if (!s.visited[v] && !s.is_blocked[v]) { + s.visited[v] = 1; + s.queue.push_back(v); + if (s.is_target[v] && --remaining == 0) { + return {}; // Early exit: all must_touch waypoints found. + } + } + } + } + + // Queue exhausted — collect unreachable must_touch IDs before the Guard + // destructs and s.visited[] is cleared. + std::vector unreachable; + for (const uint32_t id : s.dirty_target) { + if (!s.visited[id]) { + unreachable.push_back(id); + } + } + return unreachable; +} + +// Builds a lookup from dbChipRegionInst* to the corresponding UnfoldedRegion*. +std::unordered_map buildRegionInstMap( + const UnfoldedModel& model) +{ + std::unordered_map map; + for (const auto& chip : model.getChips()) { + for (const auto& [ri, ur] : chip.region_map) { + map[ri] = ur; + } + } + return map; +} + } // namespace Checker::Checker(utl::Logger* logger, dbDatabase* db) : logger_(logger), db_(db) @@ -109,6 +298,7 @@ void Checker::check() checkInternalExtUsage(top_cat, model); checkConnectionRegions(top_cat, model); checkBumpPhysicalAlignment(top_cat, model); + checkPathAssertions(top_cat, model); } void Checker::checkFloatingChips(dbMarkerCategory* top_cat, @@ -321,9 +511,115 @@ void Checker::checkBumpPhysicalAlignment(dbMarkerCategory* top_cat, } } -void Checker::checkNetConnectivity(dbMarkerCategory* top_cat, - const UnfoldedModel* model) +void Checker::checkPathAssertions(dbMarkerCategory* top_cat, + const UnfoldedModel* model) { + dbChip* chip = db_->getChip(); + auto chip_paths = chip->getChipPaths(); + if (chip_paths.empty()) { + return; + } + + auto [graph, region_to_idx] = buildRoutingGraph(*model); + if (graph.num_nodes == 0) { + return; + } + + BfsState state(graph.num_nodes); + auto region_inst_map = buildRegionInstMap(*model); + + dbMarkerCategory* cat = nullptr; + int violation_count = 0; + + for (auto* chip_path : chip_paths) { + const std::string name = chip_path->getName(); + auto entries = chip_path->getEntries(); + std::vector must_touch_ids; + std::vector do_not_touch_ids; + bool valid = true; + + for (const auto& entry : entries) { + if (!entry.region) { + logger_->warn(utl::ODB, + 473, + "Path assertion '{}' skipped: an entry has an unresolved " + "region; check earlier resolution warnings", + name); + valid = false; + break; + } + auto ri_it = region_inst_map.find(entry.region); + if (ri_it == region_inst_map.end()) { + logger_->warn( + utl::ODB, + 474, + "Path assertion '{}' skipped: region inst '{}' is not part of the " + "assembled design", + name, + entry.region->getChipRegion()->getName()); + valid = false; + break; + } + auto idx_it = region_to_idx.find(ri_it->second); + if (idx_it == region_to_idx.end()) { + logger_->error(utl::ODB, + 475, + "Path assertion '{}': region '{}' found in model but " + "missing from routing graph index — this is a bug", + name, + entry.region->getChipRegion()->getName()); + } + if (entry.negated) { + do_not_touch_ids.push_back(idx_it->second); + } else { + must_touch_ids.push_back(idx_it->second); + } + } + + if (!valid) { + continue; + } + + const auto unreachable + = assertNetRoutability(graph, must_touch_ids, do_not_touch_ids, state); + if (!unreachable.empty()) { + ++violation_count; + if (!cat) { + cat = dbMarkerCategory::createOrReplace(top_cat, "Path Assertions"); + } + auto* marker = dbMarker::create(cat); + marker->setComment(fmt::format( + "Path assertion '{}' violated: required regions are not reachable " + "while avoiding blocked regions", + name)); + + for (const auto& entry : entries) { + if (entry.negated || !entry.region) { + continue; + } + const auto ri_it = region_inst_map.find(entry.region); + if (ri_it == region_inst_map.end()) { + continue; + } + const auto idx_it = region_to_idx.find(ri_it->second); + if (idx_it == region_to_idx.end()) { + continue; + } + if (std::ranges::find(unreachable, idx_it->second) + != unreachable.end()) { + marker->addSource(entry.region); + marker->addShape(ri_it->second->cuboid); + } + } + + logger_->warn(utl::ODB, 469, "Path assertion '{}' violated", name); + } + } + + if (violation_count > 0) { + logger_->warn( + utl::ODB, 472, "Found {} path assertion(s) violated", violation_count); + } } void Checker::checkLogicalConnectivity(dbMarkerCategory* top_cat, diff --git a/src/odb/src/3dblox/checker.h b/src/odb/src/3dblox/checker.h index 404a9ec21cf..76ecc3a71e7 100644 --- a/src/odb/src/3dblox/checker.h +++ b/src/odb/src/3dblox/checker.h @@ -43,8 +43,8 @@ class Checker const UnfoldedModel* model); void checkBumpPhysicalAlignment(dbMarkerCategory* top_cat, const UnfoldedModel* model); - void checkNetConnectivity(dbMarkerCategory* top_cat, - const UnfoldedModel* model); + void checkPathAssertions(dbMarkerCategory* top_cat, + const UnfoldedModel* model); utl::Logger* logger_; dbDatabase* db_; }; diff --git a/src/odb/src/3dblox/dbxParser.cpp b/src/odb/src/3dblox/dbxParser.cpp index 39d8304fc50..64bf96f5e1a 100644 --- a/src/odb/src/3dblox/dbxParser.cpp +++ b/src/odb/src/3dblox/dbxParser.cpp @@ -3,6 +3,10 @@ #include "dbxParser.h" +#include +#include +#include + #include #include #include @@ -13,7 +17,6 @@ #include "objects.h" #include "odb/db.h" #include "utl/Logger.h" -#include "yaml-cpp/yaml.h" namespace odb { diff --git a/src/odb/src/3dblox/routingGraph.cpp b/src/odb/src/3dblox/routingGraph.cpp new file mode 100644 index 00000000000..b2ab180000d --- /dev/null +++ b/src/odb/src/3dblox/routingGraph.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023-2026, The OpenROAD Authors + +#include "routingGraph.h" + +#include +#include +#include +#include +#include + +#include "odb/unfoldedModel.h" + +namespace odb { + +// --------------------------------------------------------------------------- +// buildRoutingGraph +// +// Builds a RoutingGraph in Compressed Sparse Row (CSR) format from the +// UnfoldedModel in four passes: +// +// Pass 1 – assigns a dense uint32_t index to every UnfoldedRegion and +// builds the pointer-to-index translation map. +// Pass 2 – collects two kinds of edges and tallies per-node degrees into +// offsets[u+1]: +// (a) Inter-chip edges — one per UnfoldedConnection. +// Null-endpoint (chip-to-ground virtual) connections are +// skipped. +// (b) Intra-chip complete clique — for blackbox chips (no +// internal routing detail) the spec requires all regions +// to be treated as mutually reachable. N*(N-1)/2 edges +// are added per blackbox chip; for typical chips with two +// regions (front + back) that is exactly one extra edge. +// Pass 3 – prefix-sums offsets[] to produce CSR row-start pointers. +// Pass 4 – fills the flat neighbors[] array using a per-node write cursor. +// +// The translation map is returned alongside the graph so callers can convert +// UnfoldedRegion* to node IDs without re-scanning the chip hierarchy. +// +// Complexity: O(V + E) time and space. +// CSR layout: neighbors of node u occupy neighbors[offsets[u]..offsets[u+1]). +// All adjacency data lives in two contiguous vectors — no per-node heap alloc. +// Duplicate edges from multiple connections across one interface are harmless; +// BFS callers skip revisited nodes via their visited[] bitmask. +// --------------------------------------------------------------------------- +std::pair> +buildRoutingGraph(const UnfoldedModel& model) +{ + std::unordered_map region_to_idx; + + // Pass 1: assign dense indices. + uint32_t idx = 0; + for (const auto& chip : model.getChips()) { + for (const auto& region : chip.regions) { + region_to_idx[®ion] = idx++; + } + } + + RoutingGraph g; + g.num_nodes = idx; + g.offsets.resize(idx + 1, 0); + + if (idx == 0) { + return {std::move(g), std::move(region_to_idx)}; + } + + // Pass 2: collect edges; tally degrees into offsets[u+1] / offsets[v+1]. + struct Edge + { + uint32_t u, v; + }; + std::vector edges; + edges.reserve(model.getConnections().size()); + + // (a) Inter-chip edges from explicit connections. + for (const auto& conn : model.getConnections()) { + if (!conn.top_region || !conn.bottom_region) { + continue; // Virtual (ground) connection — no region-to-region edge. + } + const auto it_top = region_to_idx.find(conn.top_region); + const auto it_bot = region_to_idx.find(conn.bottom_region); + if (it_top == region_to_idx.end() || it_bot == region_to_idx.end()) { + continue; + } + const uint32_t u = it_top->second; + const uint32_t v = it_bot->second; + edges.push_back({u, v}); + g.offsets[u + 1]++; + g.offsets[v + 1]++; + } + + // (b) Intra-chip complete clique for blackbox chips. + for (const auto& chip : model.getChips()) { + if (!chip.is_blackbox) { + continue; + } + const auto& regions = chip.regions; + for (size_t a = 0; a < regions.size(); ++a) { + const auto it_a = region_to_idx.find(®ions[a]); + if (it_a == region_to_idx.end()) { + continue; + } + for (size_t b = a + 1; b < regions.size(); ++b) { + const auto it_b = region_to_idx.find(®ions[b]); + if (it_b == region_to_idx.end()) { + continue; + } + const uint32_t u = it_a->second; + const uint32_t v = it_b->second; + edges.push_back({u, v}); + g.offsets[u + 1]++; + g.offsets[v + 1]++; + } + } + } + + // Pass 3: prefix-sum degree counts → CSR row-start offsets. + for (uint32_t i = 1; i <= idx; ++i) { + g.offsets[i] += g.offsets[i - 1]; + } + + // Pass 4: fill neighbors[] using a per-node write cursor. + g.neighbors.resize(g.offsets[idx]); + std::vector cursor(g.offsets.begin(), g.offsets.begin() + idx); + for (const auto& [u, v] : edges) { + g.neighbors[cursor[u]++] = v; + g.neighbors[cursor[v]++] = u; + } + + return {std::move(g), std::move(region_to_idx)}; +} + +} // namespace odb diff --git a/src/odb/src/3dblox/routingGraph.h b/src/odb/src/3dblox/routingGraph.h new file mode 100644 index 00000000000..7f2073d7bec --- /dev/null +++ b/src/odb/src/3dblox/routingGraph.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023-2026, The OpenROAD Authors + +#pragma once + +#include +#include +#include +#include + +namespace odb { + +class UnfoldedModel; +struct UnfoldedRegion; + +// Flat adjacency-list routing graph in Compressed Sparse Row (CSR) format. +// Node IDs are dense uint32_t indices assigned in region-iteration order. +// The adjacency slice for node u is neighbors[offsets[u] .. offsets[u+1]). +struct RoutingGraph +{ + std::vector offsets; // size num_nodes+1; CSR row pointers + std::vector neighbors; // all adjacency lists concatenated + uint32_t num_nodes = 0; +}; + +// Builds a RoutingGraph (CSR format) from the model in O(V+E) time and space. +// Returns the graph together with the pointer-to-index translation map so +// callers can convert UnfoldedRegion* to dense node IDs without an extra pass. +// Null-endpoint connections (chip-to-ground virtuals) are skipped. +std::pair> +buildRoutingGraph(const UnfoldedModel& model); + +} // namespace odb diff --git a/src/odb/src/codeGenerator/schema/chip/dbChip.json b/src/odb/src/codeGenerator/schema/chip/dbChip.json index c641a606ece..6f4c5fbd83b 100644 --- a/src/odb/src/codeGenerator/schema/chip/dbChip.json +++ b/src/odb/src/codeGenerator/schema/chip/dbChip.json @@ -117,6 +117,12 @@ "default": "false", "schema": "kSchemaChipExtended" }, + { + "name": "blackbox_", + "type": "bool", + "default": "false", + "schema": "kSchemaChipBlackbox" + }, { "name": "top_", "type": "dbId<_dbBlock>", diff --git a/src/odb/src/db/dbChip.cpp b/src/odb/src/db/dbChip.cpp index fdafbbadd31..9262f1c694e 100644 --- a/src/odb/src/db/dbChip.cpp +++ b/src/odb/src/db/dbChip.cpp @@ -96,6 +96,9 @@ bool _dbChip::operator==(const _dbChip& rhs) const if (tsv_ != rhs.tsv_) { return false; } + if (blackbox_ != rhs.blackbox_) { + return false; + } if (top_ != rhs.top_) { return false; } @@ -169,6 +172,7 @@ _dbChip::_dbChip(_dbDatabase* db) scribe_line_north_ = 0; scribe_line_south_ = 0; tsv_ = false; + blackbox_ = false; prop_tbl_ = new dbTable<_dbProperty>( db, this, (GetObjTbl_t) &_dbChip::getObjectTable, dbPropertyObj); chip_region_tbl_ = new dbTable<_dbChipRegion>( @@ -240,6 +244,9 @@ dbIStream& operator>>(dbIStream& stream, _dbChip& obj) if (obj.getDatabase()->isSchema(kSchemaChipExtended)) { stream >> obj.tsv_; } + if (obj.getDatabase()->isSchema(kSchemaChipBlackbox)) { + stream >> obj.blackbox_; + } stream >> obj.top_; if (obj.getDatabase()->isSchema(kSchemaChipInst)) { stream >> obj.chipinsts_; @@ -304,6 +311,7 @@ dbOStream& operator<<(dbOStream& stream, const _dbChip& obj) stream << obj.scribe_line_north_; stream << obj.scribe_line_south_; stream << obj.tsv_; + stream << obj.blackbox_; stream << obj.top_; stream << obj.chipinsts_; stream << obj.conns_; @@ -577,6 +585,19 @@ bool dbChip::isTsv() const return obj->tsv_; } +void dbChip::setBlackbox(bool blackbox) +{ + _dbChip* obj = (_dbChip*) this; + + obj->blackbox_ = blackbox; +} + +bool dbChip::isBlackbox() const +{ + _dbChip* obj = (_dbChip*) this; + return obj->blackbox_; +} + dbSet dbChip::getChipRegions() const { _dbChip* obj = (_dbChip*) this; diff --git a/src/odb/src/db/dbChip.h b/src/odb/src/db/dbChip.h index 25d99a3ab3a..68b95577190 100644 --- a/src/odb/src/db/dbChip.h +++ b/src/odb/src/db/dbChip.h @@ -66,6 +66,7 @@ class _dbChip : public _dbObject int scribe_line_north_; int scribe_line_south_; bool tsv_; + bool blackbox_; dbId<_dbBlock> top_; dbTable<_dbBlock>* block_tbl_; _dbNameCache* name_cache_; diff --git a/src/odb/src/db/dbDatabase.h b/src/odb/src/db/dbDatabase.h index e6a96105487..76ec54dfa71 100644 --- a/src/odb/src/db/dbDatabase.h +++ b/src/odb/src/db/dbDatabase.h @@ -10,7 +10,6 @@ #include "dbHashTable.h" #include "odb/dbId.h" // User Code Begin Includes -#include #include #include "dbChipRegionInstItr.h" @@ -50,11 +49,14 @@ namespace odb { inline constexpr uint32_t kSchemaMajor = 0; // Not used... inline constexpr uint32_t kSchemaInitial = 57; -inline constexpr uint32_t kSchemaMinor = 128; // Current revision number +inline constexpr uint32_t kSchemaMinor = 129; // Current revision number // Revision where dbChipPath was added to dbChip inline constexpr uint32_t kSchemaChipPath = 128; +// Revision where dbChip::blackbox_ was added +inline constexpr uint32_t kSchemaChipBlackbox = 129; + // Revision where chip_bump_ back-reference was added to dbBTerm inline constexpr uint32_t kSchemaBtermChipBump = 127; diff --git a/src/odb/src/db/unfoldedModel.cpp b/src/odb/src/db/unfoldedModel.cpp index 627827bfc08..2f0123c031c 100644 --- a/src/odb/src/db/unfoldedModel.cpp +++ b/src/odb/src/db/unfoldedModel.cpp @@ -117,6 +117,7 @@ UnfoldedChip* UnfoldedModel::buildUnfoldedChip(dbChipInst* inst, uf_chip.chip_inst_path = path; uf_chip.transform = total; uf_chip.cuboid = master->getCuboid(); + uf_chip.is_blackbox = master->isBlackbox(); // Transform cuboid to global space uf_chip.transform.apply(uf_chip.cuboid); diff --git a/src/odb/test/cpp/BUILD b/src/odb/test/cpp/BUILD index 6d5fa2e2bdc..84f52a36726 100644 --- a/src/odb/test/cpp/BUILD +++ b/src/odb/test/cpp/BUILD @@ -279,6 +279,7 @@ cc_test( "Test3DBloxCheckerBumps.cpp", "Test3DBloxCheckerFixture.h", "Test3DBloxCheckerLogicalConn.cpp", + "Test3DBloxCheckerPathAssertions.cpp", ], data = [ "//src/odb/test:regression_resources", diff --git a/src/odb/test/cpp/CMakeLists.txt b/src/odb/test/cpp/CMakeLists.txt index f4f9c231aac..a06e64b4aee 100644 --- a/src/odb/test/cpp/CMakeLists.txt +++ b/src/odb/test/cpp/CMakeLists.txt @@ -36,7 +36,7 @@ add_executable(TestMaster TestMaster.cpp) add_executable(TestGDSIn TestGDSIn.cpp) add_executable(TestChips TestChips.cpp) add_executable(Test3DBloxParser Test3DBloxParser.cpp) -add_executable(Test3DBloxChecker Test3DBloxChecker.cpp Test3DBloxCheckerLogicalConn.cpp Test3DBloxCheckerBumps.cpp) +add_executable(Test3DBloxChecker Test3DBloxChecker.cpp Test3DBloxCheckerLogicalConn.cpp Test3DBloxCheckerBumps.cpp Test3DBloxCheckerPathAssertions.cpp) add_executable(Test3DBloxVerilogWriter Test3DBloxVerilogWriter.cpp) add_executable(TestSwapMaster TestSwapMaster.cpp) add_executable(TestSwapMasterUnusedPort TestSwapMasterUnusedPort.cpp) diff --git a/src/odb/test/cpp/Test3DBloxCheckerFixture.h b/src/odb/test/cpp/Test3DBloxCheckerFixture.h index 783594124b8..4d9969e6461 100644 --- a/src/odb/test/cpp/Test3DBloxCheckerFixture.h +++ b/src/odb/test/cpp/Test3DBloxCheckerFixture.h @@ -21,13 +21,16 @@ class CheckerFixture : public tst::Fixture top_chip_ = dbChip::create(db_.get(), nullptr, "TopChip", dbChip::ChipType::HIER); - // Create master chips + // Create master chips (blackbox: no DEF loaded, so all regions within each + // chip are assumed internally connected per the blackbox-stage spec). chip1_ = dbChip::create(db_.get(), tech_, "Chip1", dbChip::ChipType::DIE); + chip1_->setBlackbox(true); chip1_->setWidth(2000); chip1_->setHeight(2000); chip1_->setThickness(500); chip2_ = dbChip::create(db_.get(), tech_, "Chip2", dbChip::ChipType::DIE); + chip2_->setBlackbox(true); chip2_->setWidth(1500); chip2_->setHeight(1500); chip2_->setThickness(500); @@ -86,6 +89,7 @@ class CheckerFixture : public tst::Fixture static constexpr const char* logical_connectivity_category = "Logical Connectivity"; static constexpr const char* bump_alignment_category = "Bump Alignment"; + static constexpr const char* path_assertions_category = "Path Assertions"; }; } // namespace odb diff --git a/src/odb/test/cpp/Test3DBloxCheckerPathAssertions.cpp b/src/odb/test/cpp/Test3DBloxCheckerPathAssertions.cpp new file mode 100644 index 00000000000..b2067005433 --- /dev/null +++ b/src/odb/test/cpp/Test3DBloxCheckerPathAssertions.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2023-2026, The OpenROAD Authors + +#include +#include + +#include "Test3DBloxCheckerFixture.h" +#include "gtest/gtest.h" +#include "odb/db.h" +#include "odb/geom.h" + +namespace odb { +namespace { +// --------------------------------------------------------------------------- +// Path Assertion Tests +// +// These tests exercise Checker::checkPathAssertions, which validates +// user-specified path assertions using BFS on the region routing graph. +// Graph nodes are UnfoldedRegion objects; edges come from UnfoldedConnection. +// +// Non-negated entries are must-touch regions (must be mutually reachable). +// Negated entries are do-not-touch regions (blocked during BFS traversal). +// --------------------------------------------------------------------------- + +// 1. Connected must-touch regions → no violation. +TEST_F(CheckerFixture, test_path_assertion_connected) +{ + auto* inst1 = dbChipInst::create(top_chip_, chip1_, "inst1"); + inst1->setLoc(Point3D(0, 0, 0)); + inst1->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* inst2 = dbChipInst::create(top_chip_, chip2_, "inst2"); + inst2->setLoc(Point3D(0, 0, 500)); + inst2->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* ri1 = inst1->findChipRegionInst("r1_fr"); + auto* ri2 = inst2->findChipRegionInst("r2_bk"); + auto* conn = dbChipConn::create("c1", top_chip_, {inst1}, ri1, {inst2}, ri2); + conn->setThickness(0); + + auto* chip_path = dbChipPath::create(top_chip_, "Path1"); + chip_path->addEntry({inst1}, ri1, false); + chip_path->addEntry({inst2}, ri2, false); + + check(); + EXPECT_TRUE(getMarkers(path_assertions_category).empty()); +} + +// 2. Disconnected must-touch regions (no connection) → one violation. +TEST_F(CheckerFixture, test_path_assertion_disconnected) +{ + auto* inst1 = dbChipInst::create(top_chip_, chip1_, "inst1"); + inst1->setLoc(Point3D(0, 0, 0)); + inst1->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* inst2 = dbChipInst::create(top_chip_, chip2_, "inst2"); + inst2->setLoc(Point3D(0, 0, 500)); + inst2->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + // No connection between inst1 and inst2. + auto* ri1 = inst1->findChipRegionInst("r1_fr"); + auto* ri2 = inst2->findChipRegionInst("r2_bk"); + + auto* chip_path = dbChipPath::create(top_chip_, "Path1"); + chip_path->addEntry({inst1}, ri1, false); + chip_path->addEntry({inst2}, ri2, false); + + check(); + auto markers = getMarkers(path_assertions_category); + ASSERT_EQ(markers.size(), 1); + EXPECT_NE(markers[0]->getComment().find("Path1"), std::string::npos); + + // ri2 is unreachable from ri1 (no connection) — it must appear as a source + // and its region cuboid must be recorded as the marker shape. + auto sources = markers[0]->getSources(); + ASSERT_EQ(sources.size(), 1u); + EXPECT_EQ(sources.count(ri2), 1u); + + auto shapes = markers[0]->getShapes(); + ASSERT_EQ(shapes.size(), 1u); + EXPECT_TRUE(std::holds_alternative(shapes[0])); +} + +// 3. Do-not-touch blocks the only path → violation. +// inst1.r1_fr --c1--> inst2.r2_bk <-(blackbox clique)-> inst2.r2_fr --c2--> +// inst3.r2_bk The intra-chip clique of inst2 (blackbox) connects r2_bk and +// r2_fr. Blocking r2_bk cuts the only entry point into inst2, making +// inst3.r2_bk unreachable (r2_fr is only reachable through the blocked +// r2_bk). +TEST_F(CheckerFixture, test_path_assertion_do_not_touch_blocks) +{ + auto* inst1 = dbChipInst::create(top_chip_, chip1_, "inst1"); + inst1->setLoc(Point3D(0, 0, 0)); + inst1->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* inst2 = dbChipInst::create(top_chip_, chip2_, "inst2"); + inst2->setLoc(Point3D(0, 0, 500)); + inst2->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* inst3 = dbChipInst::create(top_chip_, chip2_, "inst3"); + inst3->setLoc(Point3D(0, 0, 1000)); + inst3->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* ri1 = inst1->findChipRegionInst("r1_fr"); + auto* ri2_bk = inst2->findChipRegionInst("r2_bk"); + auto* ri2_fr = inst2->findChipRegionInst("r2_fr"); + auto* ri3_bk = inst3->findChipRegionInst("r2_bk"); + + auto* c1 = dbChipConn::create("c1", top_chip_, {inst1}, ri1, {inst2}, ri2_bk); + c1->setThickness(0); + auto* c2 + = dbChipConn::create("c2", top_chip_, {inst2}, ri2_fr, {inst3}, ri3_bk); + c2->setThickness(0); + + auto* chip_path = dbChipPath::create(top_chip_, "Path1"); + chip_path->addEntry({inst1}, ri1, false); + chip_path->addEntry({inst2}, ri2_bk, true); // do-not-touch + chip_path->addEntry({inst3}, ri3_bk, false); + + check(); + auto markers = getMarkers(path_assertions_category); + ASSERT_EQ(markers.size(), 1); + EXPECT_NE(markers[0]->getComment().find("Path1"), std::string::npos); + + // ri3_bk is unreachable because ri2_bk (the only bridge) is blocked. + // ri2_bk itself is a do-not-touch entry, not a must-touch, so it must NOT + // appear as a source — only ri3_bk should. + auto sources = markers[0]->getSources(); + ASSERT_EQ(sources.size(), 1u); + EXPECT_EQ(sources.count(ri3_bk), 1u); + EXPECT_EQ(sources.count(ri2_bk), 0u); + + auto shapes = markers[0]->getShapes(); + ASSERT_EQ(shapes.size(), 1u); + EXPECT_TRUE(std::holds_alternative(shapes[0])); +} + +// 4. Do-not-touch on a non-critical region → no violation. +// Same topology as test 3 but blocking r2_fr (not on the path) instead. +TEST_F(CheckerFixture, test_path_assertion_do_not_touch_non_critical) +{ + auto* inst1 = dbChipInst::create(top_chip_, chip1_, "inst1"); + inst1->setLoc(Point3D(0, 0, 0)); + inst1->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* inst2 = dbChipInst::create(top_chip_, chip2_, "inst2"); + inst2->setLoc(Point3D(0, 0, 500)); + inst2->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + auto* ri1 = inst1->findChipRegionInst("r1_fr"); + auto* ri2_bk = inst2->findChipRegionInst("r2_bk"); + auto* ri2_fr = inst2->findChipRegionInst("r2_fr"); + + auto* conn + = dbChipConn::create("c1", top_chip_, {inst1}, ri1, {inst2}, ri2_bk); + conn->setThickness(0); + + // r2_fr is not on the path from r1_fr to r2_bk, so blocking it is harmless. + auto* chip_path = dbChipPath::create(top_chip_, "Path1"); + chip_path->addEntry({inst1}, ri1, false); + chip_path->addEntry({inst2}, ri2_fr, true); // do-not-touch (not on path) + chip_path->addEntry({inst2}, ri2_bk, false); + + check(); + EXPECT_TRUE(getMarkers(path_assertions_category).empty()); +} + +// 5. No path assertions → no markers. +TEST_F(CheckerFixture, test_path_assertion_empty) +{ + auto* inst1 = dbChipInst::create(top_chip_, chip1_, "inst1"); + inst1->setLoc(Point3D(0, 0, 0)); + inst1->setOrient(dbOrientType3D(dbOrientType::R0, false)); + + check(); + EXPECT_TRUE(getMarkers(path_assertions_category).empty()); +} + +} // namespace +} // namespace odb