Skip to content

Commit 6525b8d

Browse files
odb: decompose non-rectilinear LEF polygons into rectangles (#10256)
LEF POLYGON ingestion and the polygon->box->consumer chain were already intact, and rectilinear (Manhattan) polygons decomposed correctly. The real defect was non-rectilinear (45-degree/sloped) polygons: decompose_polygon fed them into Boost.Polygon's polygon_90 machinery, which only models axis-aligned geometry and corrupted sloped shapes (both dropping real metal and filling empty corners). This produced wrong obstruction/pin geometry for octagonal IO pads (e.g. sky130_ef_io__gpiov2_pad), causing the missing-geometry shorts/DRC violations vs KLayout. Fix: keep the exact polygon_90 path for rectilinear polygons (unchanged output), and for non-rectilinear polygons fracture into horizontal trapezoids and staircase each into axis-aligned rectangles, rounding outward so the result fully covers the sloped edges (never leaving gaps/shorts). Add TestGeom unit tests: rectangle, rectilinear L-shape (regression-guard the already-working path), and an octagon coverage test that fails before / passes after. Signed-off-by: Saurav Singh <saurav.singh@fermions.co>
1 parent 4c26918 commit 6525b8d

2 files changed

Lines changed: 218 additions & 3 deletions

File tree

src/odb/src/db/poly_decomp.cpp

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: BSD-3-Clause
22
// Copyright (c) 2019-2025, The OpenROAD Authors
33

4+
#include <cmath>
45
#include <limits>
56
#include <vector>
67

@@ -12,18 +13,133 @@ namespace gtl = boost::polygon;
1213

1314
namespace odb {
1415

16+
namespace {
17+
18+
// A polygon is rectilinear (Manhattan) when every edge is either purely
19+
// horizontal or purely vertical. Only such polygons can be tiled exactly by
20+
// Boost.Polygon's polygon_90 machinery.
21+
bool isRectilinear(const std::vector<Point>& points)
22+
{
23+
const size_t n = points.size();
24+
for (size_t i = 0; i < n; ++i) {
25+
const Point& a = points[i];
26+
const Point& b = points[(i + 1) % n];
27+
if (a.x() != b.x() && a.y() != b.y()) {
28+
return false;
29+
}
30+
}
31+
return true;
32+
}
33+
34+
// Decompose a single horizontal trapezoid (a polygon whose top and bottom
35+
// edges are horizontal, with up to two sloped sides) into a staircase of
36+
// axis-aligned rectangles. For each unit-tall band in Y the trapezoid's X
37+
// extent is computed from the edge crossings and rounded outward so the
38+
// resulting rectangles fully cover (never under-cover) the trapezoid. This
39+
// guarantees that obstruction/pin geometry derived from a 45-degree polygon
40+
// does not leave gaps that would later read as missing metal (shorts) for
41+
// routing/DRC consumers.
42+
void staircaseTrapezoid(const std::vector<gtl::point_data<int>>& v,
43+
std::vector<Rect>& rects)
44+
{
45+
const int n = static_cast<int>(v.size());
46+
if (n < 3) {
47+
return;
48+
}
49+
50+
int y_lo = std::numeric_limits<int>::max();
51+
int y_hi = std::numeric_limits<int>::min();
52+
for (const auto& p : v) {
53+
y_lo = std::min(y_lo, gtl::y(p));
54+
y_hi = std::max(y_hi, gtl::y(p));
55+
}
56+
57+
for (int y = y_lo; y < y_hi; ++y) {
58+
const double y_mid = y + 0.5;
59+
double x_min = std::numeric_limits<double>::max();
60+
double x_max = std::numeric_limits<double>::lowest();
61+
62+
for (int i = 0; i < n; ++i) {
63+
const auto& a = v[i];
64+
const auto& b = v[(i + 1) % n];
65+
const int ay = gtl::y(a);
66+
const int by = gtl::y(b);
67+
// Edge crosses the band's mid-line?
68+
if ((ay <= y_mid && by > y_mid) || (by <= y_mid && ay > y_mid)) {
69+
const double t = (y_mid - ay) / static_cast<double>(by - ay);
70+
const double x = gtl::x(a) + t * (gtl::x(b) - gtl::x(a));
71+
x_min = std::min(x_min, x);
72+
x_max = std::max(x_max, x);
73+
}
74+
}
75+
76+
if (x_max <= x_min) {
77+
continue;
78+
}
79+
80+
// Round outward so the staircase fully covers the sloped edges.
81+
const int x_lo = static_cast<int>(std::floor(x_min));
82+
const int x_hi = static_cast<int>(std::ceil(x_max));
83+
if (x_hi <= x_lo) {
84+
continue;
85+
}
86+
87+
// Coalesce with the previous band when it has the same X extent and is
88+
// vertically contiguous, to keep the rectangle count down for vertical
89+
// runs.
90+
if (!rects.empty() && rects.back().xMin() == x_lo
91+
&& rects.back().xMax() == x_hi && rects.back().yMax() == y) {
92+
rects.back().set_yhi(y + 1);
93+
} else {
94+
rects.emplace_back(x_lo, y, x_hi, y + 1);
95+
}
96+
}
97+
}
98+
99+
} // namespace
100+
15101
void decompose_polygon(const std::vector<Point>& points,
16102
std::vector<Rect>& rects)
17103
{
18104
using boost::polygon::operators::operator+=;
19105

20-
gtl::polygon_90_data<int> polygon;
106+
// Fast, exact path for Manhattan polygons (the common case: obstructions,
107+
// pin ports, and die-area blockages are almost always rectilinear). This
108+
// preserves the previous behaviour and output exactly.
109+
if (isRectilinear(points)) {
110+
gtl::polygon_90_data<int> polygon;
111+
polygon.set(points.begin(), points.end());
112+
113+
gtl::polygon_90_set_data<int> polygon_set;
114+
polygon_set += polygon;
115+
116+
polygon_set.get_rectangles(rects);
117+
return;
118+
}
119+
120+
// Non-rectilinear (e.g. 45-degree / octagonal pad) polygons cannot be
121+
// expressed by polygon_90 without corrupting the shape (it fills corners
122+
// that should be empty and leaves real metal uncovered). Fracture the
123+
// general polygon into horizontal trapezoids and staircase each one into
124+
// axis-aligned rectangles.
125+
gtl::polygon_data<int> polygon;
21126
polygon.set(points.begin(), points.end());
22127

23-
gtl::polygon_90_set_data<int> polygon_set;
128+
gtl::polygon_set_data<int> polygon_set;
24129
polygon_set += polygon;
25130

26-
polygon_set.get_rectangles(rects);
131+
std::vector<gtl::polygon_data<int>> trapezoids;
132+
polygon_set.get_trapezoids(trapezoids, gtl::HORIZONTAL);
133+
134+
for (const auto& trapezoid : trapezoids) {
135+
std::vector<gtl::point_data<int>> v(trapezoid.begin(), trapezoid.end());
136+
// Boost may repeat the closing vertex; drop it so edge iteration is clean.
137+
if (v.size() > 1 && gtl::x(v.front()) == gtl::x(v.back())
138+
&& gtl::y(v.front()) == gtl::y(v.back())) {
139+
v.pop_back();
140+
}
141+
staircaseTrapezoid(v, rects);
142+
}
27143
}
28144

29145
// See "Orientation of a simple polygon" in

src/odb/test/cpp/TestGeom.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,109 @@
33
#include "gtest/gtest.h"
44
#include "odb/geom.h"
55
#include "odb/isotropy.h"
6+
#include "odb/poly_decomp.h"
67

78
namespace odb {
89
namespace {
910

11+
// Even-odd point-in-polygon test on the polygon interior (using the sample
12+
// point's center). Used to validate that the rectangle decomposition covers
13+
// the same area as the source polygon.
14+
bool pointInPolygon(const std::vector<Point>& poly, double x, double y)
15+
{
16+
bool inside = false;
17+
const int n = static_cast<int>(poly.size());
18+
for (int i = 0, j = n - 1; i < n; j = i++) {
19+
const double xi = poly[i].x(), yi = poly[i].y();
20+
const double xj = poly[j].x(), yj = poly[j].y();
21+
if (((yi > y) != (yj > y))
22+
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
23+
inside = !inside;
24+
}
25+
}
26+
return inside;
27+
}
28+
29+
bool pointInRects(const std::vector<Rect>& rects, int x, int y)
30+
{
31+
for (const Rect& r : rects) {
32+
if (x >= r.xMin() && x < r.xMax() && y >= r.yMin() && y < r.yMax()) {
33+
return true;
34+
}
35+
}
36+
return false;
37+
}
38+
39+
// A simple rectangle decomposes to itself.
40+
TEST(geom, decompose_rectangle)
41+
{
42+
const std::vector<Point> rect = {{0, 0}, {100, 0}, {100, 50}, {0, 50}};
43+
std::vector<Rect> rects;
44+
decompose_polygon(rect, rects);
45+
46+
ASSERT_EQ(rects.size(), 1u);
47+
EXPECT_EQ(rects[0], Rect(0, 0, 100, 50));
48+
}
49+
50+
// A rectilinear (Manhattan) L-shape still uses the exact polygon_90 path and
51+
// decomposes into two rectangles, unchanged by the non-rectilinear support.
52+
TEST(geom, decompose_rectilinear_l_shape)
53+
{
54+
const std::vector<Point> l_shape
55+
= {{0, 0}, {20, 0}, {20, 10}, {10, 10}, {10, 20}, {0, 20}};
56+
std::vector<Rect> rects;
57+
decompose_polygon(l_shape, rects);
58+
59+
ASSERT_EQ(rects.size(), 2u);
60+
EXPECT_EQ(rects[0], Rect(0, 0, 20, 10));
61+
EXPECT_EQ(rects[1], Rect(0, 10, 10, 20));
62+
}
63+
64+
// Regression for OpenROAD #10256: a 45-degree (octagonal) polygon, like the
65+
// pad geometry in sky130/gscl45 IO cells, must be decomposed into rectangles
66+
// that fully cover the polygon. The previous polygon_90-only implementation
67+
// corrupted the shape, leaving real metal uncovered (which manifested as
68+
// missing obstructions/pins -> shorts) and filling empty corners.
69+
TEST(geom, decompose_octagon_covers_polygon)
70+
{
71+
// gscl45nm_polygon.lef metal5 PAD octagon, scaled to integer DBU.
72+
const std::vector<Point> octagon = {{1450, 542},
73+
{542, 1450},
74+
{-542, 1450},
75+
{-1450, 542},
76+
{-1450, -542},
77+
{-542, -1450},
78+
{542, -1450},
79+
{1450, -542}};
80+
81+
std::vector<Rect> rects;
82+
decompose_polygon(octagon, rects);
83+
84+
// The decomposition must produce geometry (the bug dropped/corrupted it).
85+
ASSERT_FALSE(rects.empty());
86+
87+
// Every point inside the octagon must be covered by some rectangle
88+
// (no under-coverage), and no point outside it should be covered
89+
// (no over-coverage).
90+
int undercovered = 0;
91+
int overcovered = 0;
92+
for (int y = -1450; y < 1450; y += 11) {
93+
for (int x = -1450; x < 1450; x += 11) {
94+
const bool in_poly = pointInPolygon(octagon, x + 0.5, y + 0.5);
95+
const bool in_rects = pointInRects(rects, x, y);
96+
if (in_poly && !in_rects) {
97+
++undercovered;
98+
}
99+
if (!in_poly && in_rects) {
100+
++overcovered;
101+
}
102+
}
103+
}
104+
105+
EXPECT_EQ(undercovered, 0);
106+
EXPECT_EQ(overcovered, 0);
107+
}
108+
10109
TEST(geom, test_oct)
11110
{
12111
Oct oct;

0 commit comments

Comments
 (0)