@tscircuit/fanout-solver 0.0.59 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fanout-solver.ts +600 -10
- package/lib/match-bus-lengths.ts +25 -1
- package/lib/match-component-dogbone-via-sites.ts +27 -0
- package/lib/route-bus.ts +288 -56
- package/lib/route-via-minimal-winding.ts +50 -1
- package/package.json +1 -1
package/lib/fanout-solver.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { refineAdaptivePlaneReservationCore } from "./refine-adaptive-plane-rese
|
|
|
5
5
|
import { shouldUseAdaptiveDensePlaneRouting } from "./should-use-adaptive-dense-plane-routing"
|
|
6
6
|
import { type GraphicsObject, mergeGraphics } from "graphics-debug"
|
|
7
7
|
import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getCornerBandSide,
|
|
10
|
+
getDirectionForExitEdge,
|
|
11
|
+
getExitEdgeForDirection,
|
|
12
|
+
} from "./boundary-exit"
|
|
9
13
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
10
14
|
import {
|
|
11
15
|
type CompleteOriginalEndpointsResult,
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
20
24
|
import {
|
|
21
25
|
getComponentDogboneViaSiteCandidates,
|
|
26
|
+
getSingleDogboneViaSiteRepairs,
|
|
22
27
|
matchComponentDogboneViaSites,
|
|
23
28
|
} from "./match-component-dogbone-via-sites"
|
|
24
29
|
import { connectionsShareElectricalNet } from "./net-identity"
|
|
@@ -463,6 +468,9 @@ function createInitialLayerAssignment(params: {
|
|
|
463
468
|
escapeLayers: string[]
|
|
464
469
|
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
465
470
|
preferOrderedCoordinatedWindingLayers: boolean
|
|
471
|
+
traceWidth: number
|
|
472
|
+
viaDiameter: number
|
|
473
|
+
clearance: number
|
|
466
474
|
}): Readonly<Record<string, string>> {
|
|
467
475
|
const {
|
|
468
476
|
buses,
|
|
@@ -509,10 +517,151 @@ function createInitialLayerAssignment(params: {
|
|
|
509
517
|
preferOrderedCoordinatedWindingLayers &&
|
|
510
518
|
busUsesCoordinatedWinding(bus)
|
|
511
519
|
) {
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
520
|
+
// Preserve caller preferences when boundary corridors are free. A
|
|
521
|
+
// centered or turning bus with a common target layer can occupy the
|
|
522
|
+
// same boundary band, so prefer another legal layer for a wide route.
|
|
523
|
+
const cornerSide = getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
524
|
+
const getBoundaryCongestion = (layer: string): number => {
|
|
525
|
+
if (!bus.exitEdge || bus.connections.length < 8) return 0
|
|
526
|
+
const horizontalEdge =
|
|
527
|
+
bus.exitEdge === "left" || bus.exitEdge === "right"
|
|
528
|
+
const axis = horizontalEdge ? "y" : "x"
|
|
529
|
+
const minimum = horizontalEdge
|
|
530
|
+
? bus.sharedBoundary.minY
|
|
531
|
+
: bus.sharedBoundary.minX
|
|
532
|
+
const maximum = horizontalEdge
|
|
533
|
+
? bus.sharedBoundary.maxY
|
|
534
|
+
: bus.sharedBoundary.maxX
|
|
535
|
+
if (!cornerSide) {
|
|
536
|
+
const tracks = bus.connections.map((connection) => {
|
|
537
|
+
const target =
|
|
538
|
+
connection.exitTargetPoint ?? connection.targetPoint
|
|
539
|
+
return Math.max(minimum, Math.min(maximum, target[axis]))
|
|
540
|
+
})
|
|
541
|
+
const bandMinimum = Math.min(...tracks)
|
|
542
|
+
const bandMaximum = Math.max(...tracks)
|
|
543
|
+
const forwardAxis = horizontalEdge ? "x" : "y"
|
|
544
|
+
const sign =
|
|
545
|
+
bus.exitEdge === "right" || bus.exitEdge === "top" ? 1 : -1
|
|
546
|
+
const sourceNearEnd = Math.min(
|
|
547
|
+
...bus.connections.map(
|
|
548
|
+
(connection) => sign * connection.sourcePoint[forwardAxis],
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
const sourceMinimum = Math.min(
|
|
552
|
+
...bus.connections.map(
|
|
553
|
+
(connection) => connection.sourcePoint[axis],
|
|
554
|
+
),
|
|
555
|
+
)
|
|
556
|
+
const sourceMaximum = Math.max(
|
|
557
|
+
...bus.connections.map(
|
|
558
|
+
(connection) => connection.sourcePoint[axis],
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
return buses.reduce((count, other) => {
|
|
562
|
+
const otherCorner = getCornerBandSide(
|
|
563
|
+
other.exitEdge,
|
|
564
|
+
other.preferredExit,
|
|
565
|
+
)
|
|
566
|
+
if (
|
|
567
|
+
other === bus ||
|
|
568
|
+
other.termination.type !== "boundary" ||
|
|
569
|
+
other.componentId !== bus.componentId ||
|
|
570
|
+
other.exitEdge !== bus.exitEdge ||
|
|
571
|
+
!otherCorner ||
|
|
572
|
+
getCommonExplicitExitTargetLayer(other) !== layer
|
|
573
|
+
)
|
|
574
|
+
return count
|
|
575
|
+
const center =
|
|
576
|
+
minimum +
|
|
577
|
+
(maximum - minimum) * (otherCorner === "minimum" ? 0.25 : 0.75)
|
|
578
|
+
const halfWidth =
|
|
579
|
+
((Math.max(
|
|
580
|
+
other.connections.length,
|
|
581
|
+
other.cornerBandConnectionCount ?? 0,
|
|
582
|
+
) -
|
|
583
|
+
1) *
|
|
584
|
+
Math.max(
|
|
585
|
+
params.traceWidth + params.clearance,
|
|
586
|
+
params.viaDiameter + params.clearance,
|
|
587
|
+
)) /
|
|
588
|
+
2
|
|
589
|
+
const margin = params.traceWidth + params.clearance
|
|
590
|
+
const overlapsBoundaryBand =
|
|
591
|
+
bandMaximum + margin >= center - halfWidth &&
|
|
592
|
+
bandMinimum - margin <= center + halfWidth
|
|
593
|
+
// A turning bus behind this source field must also pass its lanes
|
|
594
|
+
// on the way to the edge, even when its final band is elsewhere.
|
|
595
|
+
const crossesSourceField =
|
|
596
|
+
Math.max(
|
|
597
|
+
...other.connections.map(
|
|
598
|
+
(connection) => sign * connection.sourcePoint[forwardAxis],
|
|
599
|
+
),
|
|
600
|
+
) <
|
|
601
|
+
sourceNearEnd - 1e-9 &&
|
|
602
|
+
Math.min(
|
|
603
|
+
...other.connections.map(
|
|
604
|
+
(connection) => connection.sourcePoint[axis],
|
|
605
|
+
),
|
|
606
|
+
) <=
|
|
607
|
+
sourceMaximum + margin &&
|
|
608
|
+
Math.max(
|
|
609
|
+
...other.connections.map(
|
|
610
|
+
(connection) => connection.sourcePoint[axis],
|
|
611
|
+
),
|
|
612
|
+
) >=
|
|
613
|
+
sourceMinimum - margin
|
|
614
|
+
return (
|
|
615
|
+
count +
|
|
616
|
+
(overlapsBoundaryBand || crossesSourceField
|
|
617
|
+
? other.connections.length
|
|
618
|
+
: 0)
|
|
619
|
+
)
|
|
620
|
+
}, 0)
|
|
621
|
+
}
|
|
622
|
+
const bandCenter =
|
|
623
|
+
minimum +
|
|
624
|
+
(maximum - minimum) * (cornerSide === "minimum" ? 0.25 : 0.75)
|
|
625
|
+
const pitch = Math.max(
|
|
626
|
+
params.traceWidth + params.clearance,
|
|
627
|
+
params.viaDiameter + params.clearance,
|
|
628
|
+
)
|
|
629
|
+
const bandHalfWidth =
|
|
630
|
+
((Math.max(
|
|
631
|
+
bus.connections.length,
|
|
632
|
+
bus.cornerBandConnectionCount ?? 0,
|
|
633
|
+
) -
|
|
634
|
+
1) *
|
|
635
|
+
pitch) /
|
|
636
|
+
2
|
|
637
|
+
return buses.reduce((count, other) => {
|
|
638
|
+
if (
|
|
639
|
+
other === bus ||
|
|
640
|
+
other.termination.type !== "boundary" ||
|
|
641
|
+
other.componentId !== bus.componentId ||
|
|
642
|
+
other.exitEdge !== bus.exitEdge ||
|
|
643
|
+
getCornerBandSide(other.exitEdge, other.preferredExit) ||
|
|
644
|
+
getCommonExplicitExitTargetLayer(other) !== layer
|
|
645
|
+
)
|
|
646
|
+
return count
|
|
647
|
+
return (
|
|
648
|
+
count +
|
|
649
|
+
other.connections.filter((connection) => {
|
|
650
|
+
const target =
|
|
651
|
+
connection.exitTargetPoint ?? connection.targetPoint
|
|
652
|
+
const track = Math.max(minimum, Math.min(maximum, target[axis]))
|
|
653
|
+
return (
|
|
654
|
+
Math.abs(track - bandCenter) <=
|
|
655
|
+
bandHalfWidth + params.traceWidth + params.clearance
|
|
656
|
+
)
|
|
657
|
+
}).length
|
|
658
|
+
)
|
|
659
|
+
}, 0)
|
|
660
|
+
}
|
|
661
|
+
assignment[bus.busId] = viaLayers.toSorted(
|
|
662
|
+
(first, second) =>
|
|
663
|
+
getBoundaryCongestion(first) - getBoundaryCongestion(second),
|
|
664
|
+
)[0]!
|
|
516
665
|
continue
|
|
517
666
|
}
|
|
518
667
|
const componentDirections = directionsByComponent.get(bus.componentId)!
|
|
@@ -1042,6 +1191,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1042
1191
|
buses: this.preparedBuses,
|
|
1043
1192
|
escapeLayers: this.config.escapeLayers,
|
|
1044
1193
|
escapeLayersByBusId: this.escapeLayersByBusId,
|
|
1194
|
+
traceWidth: this.config.traceWidth,
|
|
1195
|
+
viaDiameter: this.config.viaDiameter,
|
|
1196
|
+
clearance: this.config.clearance,
|
|
1045
1197
|
preferOrderedCoordinatedWindingLayers:
|
|
1046
1198
|
this.config.densePlaneReservationBusIds.length > 0 ||
|
|
1047
1199
|
this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
|
|
@@ -1482,6 +1634,64 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1482
1634
|
const wideBoundaryBuses = unsortedBoundaryBuses.filter(
|
|
1483
1635
|
(bus) => bus.connections.length >= 8,
|
|
1484
1636
|
)
|
|
1637
|
+
// A single-layer turning bus beside the end of a centered source field
|
|
1638
|
+
// has fewer escape choices than a corner bus farther behind it. Reserve
|
|
1639
|
+
// that turning channel before the farther bus fences its local via sites.
|
|
1640
|
+
const adjacentCenteredFieldByTurningBus = new Map<
|
|
1641
|
+
PreparedBus,
|
|
1642
|
+
PreparedBus
|
|
1643
|
+
>()
|
|
1644
|
+
if (usePadAlignedDenseRouting && !configuredDensePlaneRouting) {
|
|
1645
|
+
for (const bus of wideBoundaryBuses) {
|
|
1646
|
+
if (
|
|
1647
|
+
!getCornerBandSide(bus.exitEdge, bus.preferredExit) ||
|
|
1648
|
+
new Set(bus.routableEscapeLayers ?? bus.allowedLayers ?? []).size !==
|
|
1649
|
+
1
|
|
1650
|
+
)
|
|
1651
|
+
continue
|
|
1652
|
+
const axis =
|
|
1653
|
+
bus.direction === "up" || bus.direction === "down" ? "y" : "x"
|
|
1654
|
+
const track = axis === "x" ? "y" : "x"
|
|
1655
|
+
const sign =
|
|
1656
|
+
bus.direction === "up" || bus.direction === "right" ? 1 : -1
|
|
1657
|
+
const forwardEnd = Math.max(
|
|
1658
|
+
...bus.connections.map((c) => sign * c.sourcePoint[axis]),
|
|
1659
|
+
)
|
|
1660
|
+
const minTrack = Math.min(
|
|
1661
|
+
...bus.connections.map((c) => c.sourcePoint[track]),
|
|
1662
|
+
)
|
|
1663
|
+
const maxTrack = Math.max(
|
|
1664
|
+
...bus.connections.map((c) => c.sourcePoint[track]),
|
|
1665
|
+
)
|
|
1666
|
+
const pitch = axis === "x" ? bus.pitchX : bus.pitchY
|
|
1667
|
+
const field = wideBoundaryBuses.find((candidate) => {
|
|
1668
|
+
if (
|
|
1669
|
+
candidate === bus ||
|
|
1670
|
+
candidate.componentId !== bus.componentId ||
|
|
1671
|
+
candidate.exitEdge !== bus.exitEdge ||
|
|
1672
|
+
getCornerBandSide(candidate.exitEdge, candidate.preferredExit)
|
|
1673
|
+
)
|
|
1674
|
+
return false
|
|
1675
|
+
const nearEnd = Math.min(
|
|
1676
|
+
...candidate.connections.map((c) => sign * c.sourcePoint[axis]),
|
|
1677
|
+
)
|
|
1678
|
+
const gap = nearEnd - forwardEnd
|
|
1679
|
+
return (
|
|
1680
|
+
gap >= -1e-9 &&
|
|
1681
|
+
gap <= pitch + 1e-9 &&
|
|
1682
|
+
Math.min(
|
|
1683
|
+
...candidate.connections.map((c) => c.sourcePoint[track]),
|
|
1684
|
+
) <=
|
|
1685
|
+
maxTrack + 1e-9 &&
|
|
1686
|
+
Math.max(
|
|
1687
|
+
...candidate.connections.map((c) => c.sourcePoint[track]),
|
|
1688
|
+
) >=
|
|
1689
|
+
minTrack - 1e-9
|
|
1690
|
+
)
|
|
1691
|
+
})
|
|
1692
|
+
if (field) adjacentCenteredFieldByTurningBus.set(bus, field)
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1485
1695
|
const hasThreeWideBoundaryBuses =
|
|
1486
1696
|
useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
|
|
1487
1697
|
const getBoundaryTargetSpan = (bus: PreparedBus) => {
|
|
@@ -1982,14 +2192,35 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1982
2192
|
: []),
|
|
1983
2193
|
...singletonDeferralCandidates.filter((bus) => {
|
|
1984
2194
|
const containingBus = getContainingWideSourceField(bus)
|
|
1985
|
-
const
|
|
2195
|
+
const sourcePoint = bus.connections[0]!.sourcePoint
|
|
2196
|
+
const componentCenter = {
|
|
2197
|
+
x: (bus.componentBounds.minX + bus.componentBounds.maxX) / 2,
|
|
2198
|
+
y: (bus.componentBounds.minY + bus.componentBounds.maxY) / 2,
|
|
2199
|
+
}
|
|
2200
|
+
const boundaryDirection = bus.exitEdge
|
|
2201
|
+
? getDirectionForExitEdge(bus.exitEdge)
|
|
2202
|
+
: bus.direction
|
|
2203
|
+
const inwardProjection =
|
|
2204
|
+
boundaryDirection === "right"
|
|
2205
|
+
? componentCenter.x - sourcePoint.x
|
|
2206
|
+
: boundaryDirection === "left"
|
|
2207
|
+
? sourcePoint.x - componentCenter.x
|
|
2208
|
+
: boundaryDirection === "up"
|
|
2209
|
+
? componentCenter.y - sourcePoint.y
|
|
2210
|
+
: sourcePoint.y - componentCenter.y
|
|
2211
|
+
// Crossing the component can consume an embedded singleton's source
|
|
2212
|
+
// dogbone even when the target layers differ. Keep outward boundary
|
|
2213
|
+
// escapes provisional unless they share the wide bus's target layer.
|
|
2214
|
+
const reserveEmbeddedSourceEscape =
|
|
1986
2215
|
usePadAlignedDenseRouting &&
|
|
1987
2216
|
!useConfiguredDensePlaneRouting &&
|
|
1988
2217
|
containingBus &&
|
|
1989
|
-
params.busLayerAssignments[containingBus.busId] ===
|
|
1990
|
-
params.busLayerAssignments[bus.busId]
|
|
2218
|
+
(params.busLayerAssignments[containingBus.busId] ===
|
|
2219
|
+
params.busLayerAssignments[bus.busId] ||
|
|
2220
|
+
inwardProjection > 1e-9)
|
|
1991
2221
|
return (
|
|
1992
|
-
!leadingWideSingletonBuses.includes(bus) &&
|
|
2222
|
+
!leadingWideSingletonBuses.includes(bus) &&
|
|
2223
|
+
!reserveEmbeddedSourceEscape
|
|
1993
2224
|
)
|
|
1994
2225
|
}),
|
|
1995
2226
|
...(hasThreeWideBoundaryBuses
|
|
@@ -2164,6 +2395,167 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2164
2395
|
bus,
|
|
2165
2396
|
]),
|
|
2166
2397
|
]
|
|
2398
|
+
for (const [bus] of adjacentCenteredFieldByTurningBus) {
|
|
2399
|
+
const axis =
|
|
2400
|
+
bus.direction === "up" || bus.direction === "down" ? "y" : "x"
|
|
2401
|
+
const sign =
|
|
2402
|
+
bus.direction === "up" || bus.direction === "right" ? 1 : -1
|
|
2403
|
+
const backwardEnd = Math.min(
|
|
2404
|
+
...bus.connections.map(
|
|
2405
|
+
(connection) => sign * connection.sourcePoint[axis],
|
|
2406
|
+
),
|
|
2407
|
+
)
|
|
2408
|
+
const index = denseBoundaryBusesInRoutingOrder.indexOf(bus)
|
|
2409
|
+
const earlierCornerIndex = denseBoundaryBusesInRoutingOrder.findIndex(
|
|
2410
|
+
(candidate) =>
|
|
2411
|
+
candidate !== bus &&
|
|
2412
|
+
candidate.connections.length >= 8 &&
|
|
2413
|
+
candidate.componentId === bus.componentId &&
|
|
2414
|
+
candidate.direction === bus.direction &&
|
|
2415
|
+
Math.max(
|
|
2416
|
+
...candidate.connections.map(
|
|
2417
|
+
(connection) => sign * connection.sourcePoint[axis],
|
|
2418
|
+
),
|
|
2419
|
+
) <=
|
|
2420
|
+
backwardEnd + 1e-9 &&
|
|
2421
|
+
candidate.exitEdge === bus.exitEdge &&
|
|
2422
|
+
getCornerBandSide(candidate.exitEdge, candidate.preferredExit) ===
|
|
2423
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit),
|
|
2424
|
+
)
|
|
2425
|
+
if (earlierCornerIndex >= 0 && earlierCornerIndex < index) {
|
|
2426
|
+
denseBoundaryBusesInRoutingOrder.splice(index, 1)
|
|
2427
|
+
denseBoundaryBusesInRoutingOrder.splice(earlierCornerIndex, 0, bus)
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const areAdjacentInvertedNarrowBuses = (
|
|
2431
|
+
first: PreparedBus,
|
|
2432
|
+
second: PreparedBus,
|
|
2433
|
+
): boolean => {
|
|
2434
|
+
if (
|
|
2435
|
+
!usePadAlignedDenseRouting ||
|
|
2436
|
+
first.componentId !== second.componentId ||
|
|
2437
|
+
first.exitEdge !== second.exitEdge ||
|
|
2438
|
+
first.direction !== second.direction ||
|
|
2439
|
+
getCornerBandSide(first.exitEdge, first.preferredExit) !==
|
|
2440
|
+
getCornerBandSide(second.exitEdge, second.preferredExit) ||
|
|
2441
|
+
params.busLayerAssignments[first.busId] !==
|
|
2442
|
+
params.busLayerAssignments[second.busId] ||
|
|
2443
|
+
getContainingWideSourceField(first) !==
|
|
2444
|
+
getContainingWideSourceField(second) ||
|
|
2445
|
+
!getContainingWideSourceField(first)
|
|
2446
|
+
)
|
|
2447
|
+
return false
|
|
2448
|
+
const firstCenter = {
|
|
2449
|
+
x:
|
|
2450
|
+
first.connections.reduce(
|
|
2451
|
+
(sum, connection) => sum + connection.sourcePoint.x,
|
|
2452
|
+
0,
|
|
2453
|
+
) / first.connections.length,
|
|
2454
|
+
y:
|
|
2455
|
+
first.connections.reduce(
|
|
2456
|
+
(sum, connection) => sum + connection.sourcePoint.y,
|
|
2457
|
+
0,
|
|
2458
|
+
) / first.connections.length,
|
|
2459
|
+
}
|
|
2460
|
+
const secondCenter = {
|
|
2461
|
+
x:
|
|
2462
|
+
second.connections.reduce(
|
|
2463
|
+
(sum, connection) => sum + connection.sourcePoint.x,
|
|
2464
|
+
0,
|
|
2465
|
+
) / second.connections.length,
|
|
2466
|
+
y:
|
|
2467
|
+
second.connections.reduce(
|
|
2468
|
+
(sum, connection) => sum + connection.sourcePoint.y,
|
|
2469
|
+
0,
|
|
2470
|
+
) / second.connections.length,
|
|
2471
|
+
}
|
|
2472
|
+
if (
|
|
2473
|
+
Math.hypot(
|
|
2474
|
+
firstCenter.x - secondCenter.x,
|
|
2475
|
+
firstCenter.y - secondCenter.y,
|
|
2476
|
+
) >
|
|
2477
|
+
1.5 * Math.min(first.pitchX, first.pitchY)
|
|
2478
|
+
)
|
|
2479
|
+
return false
|
|
2480
|
+
const axis =
|
|
2481
|
+
first.exitEdge === "left" || first.exitEdge === "right" ? "y" : "x"
|
|
2482
|
+
const targetTrack = (bus: PreparedBus) =>
|
|
2483
|
+
bus.connections.reduce(
|
|
2484
|
+
(sum, connection) =>
|
|
2485
|
+
sum +
|
|
2486
|
+
(connection.exitTargetPoint ?? connection.targetPoint)[axis],
|
|
2487
|
+
0,
|
|
2488
|
+
) / bus.connections.length
|
|
2489
|
+
return (
|
|
2490
|
+
(firstCenter[axis] - secondCenter[axis]) *
|
|
2491
|
+
(targetTrack(first) - targetTrack(second)) <
|
|
2492
|
+
-1e-9
|
|
2493
|
+
)
|
|
2494
|
+
}
|
|
2495
|
+
// Preserve a centered pair's channel before its leading singleton. A
|
|
2496
|
+
// turning pair instead leaves its adjacent singleton room to escape
|
|
2497
|
+
// before searching for an outside-package via.
|
|
2498
|
+
const centeredPairPromotionGroups = new Set<string>()
|
|
2499
|
+
for (const singleton of leadingWideSingletonBuses) {
|
|
2500
|
+
if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
|
|
2501
|
+
continue
|
|
2502
|
+
for (const pair of boundaryBuses) {
|
|
2503
|
+
if (
|
|
2504
|
+
pair.connections.length !== 2 ||
|
|
2505
|
+
!areAdjacentInvertedNarrowBuses(pair, singleton)
|
|
2506
|
+
)
|
|
2507
|
+
continue
|
|
2508
|
+
const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
|
|
2509
|
+
const singletonIndex =
|
|
2510
|
+
denseBoundaryBusesInRoutingOrder.indexOf(singleton)
|
|
2511
|
+
if (pairIndex > singletonIndex) {
|
|
2512
|
+
denseBoundaryBusesInRoutingOrder.splice(pairIndex, 1)
|
|
2513
|
+
denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 0, pair)
|
|
2514
|
+
centeredPairPromotionGroups.add(
|
|
2515
|
+
`${pair.componentId}:${pair.exitEdge}`,
|
|
2516
|
+
)
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
for (const pair of boundaryBuses) {
|
|
2521
|
+
if (
|
|
2522
|
+
!centeredPairPromotionGroups.has(
|
|
2523
|
+
`${pair.componentId}:${pair.exitEdge}`,
|
|
2524
|
+
) ||
|
|
2525
|
+
pair.connections.length !== 2 ||
|
|
2526
|
+
!getCornerBandSide(pair.exitEdge, pair.preferredExit)
|
|
2527
|
+
)
|
|
2528
|
+
continue
|
|
2529
|
+
for (const singleton of singletonBoundaryBuses) {
|
|
2530
|
+
if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
|
|
2531
|
+
const singletonIndex =
|
|
2532
|
+
denseBoundaryBusesInRoutingOrder.indexOf(singleton)
|
|
2533
|
+
const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
|
|
2534
|
+
if (singletonIndex > pairIndex) {
|
|
2535
|
+
denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
|
|
2536
|
+
denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
for (const field of adjacentCenteredFieldByTurningBus.values()) {
|
|
2541
|
+
for (const pair of boundaryBuses) {
|
|
2542
|
+
if (
|
|
2543
|
+
pair.connections.length !== 2 ||
|
|
2544
|
+
getContainingWideSourceField(pair) !== field
|
|
2545
|
+
)
|
|
2546
|
+
continue
|
|
2547
|
+
for (const singleton of singletonBoundaryBuses) {
|
|
2548
|
+
if (!areAdjacentInvertedNarrowBuses(singleton, pair)) continue
|
|
2549
|
+
const pairIndex = denseBoundaryBusesInRoutingOrder.indexOf(pair)
|
|
2550
|
+
const singletonIndex =
|
|
2551
|
+
denseBoundaryBusesInRoutingOrder.indexOf(singleton)
|
|
2552
|
+
if (singletonIndex > pairIndex) {
|
|
2553
|
+
denseBoundaryBusesInRoutingOrder.splice(singletonIndex, 1)
|
|
2554
|
+
denseBoundaryBusesInRoutingOrder.splice(pairIndex, 0, singleton)
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2167
2559
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
2168
2560
|
number,
|
|
2169
2561
|
{ x: number; y: number }
|
|
@@ -2311,6 +2703,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2311
2703
|
useConfiguredDensePlaneRouting &&
|
|
2312
2704
|
singleLayerBus !== bus &&
|
|
2313
2705
|
!embeddedNarrowBusAlreadyRouted
|
|
2706
|
+
let usedRepairedViaSites = false
|
|
2314
2707
|
let busPlans = (yield* routeAlternatives(
|
|
2315
2708
|
preferSingleLayerWinding
|
|
2316
2709
|
? { ...routeParams, bus: singleLayerBus }
|
|
@@ -2417,6 +2810,197 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2417
2810
|
}
|
|
2418
2811
|
}
|
|
2419
2812
|
}
|
|
2813
|
+
// A first turning bus can be fenced by one diagonal site even though
|
|
2814
|
+
// every provisional dogbone is individually legal. Try moving one of
|
|
2815
|
+
// its own sites while retaining all other through-via reservations.
|
|
2816
|
+
// Keep this bounded repair ahead of the broader free-site search.
|
|
2817
|
+
if (
|
|
2818
|
+
!busPlans &&
|
|
2819
|
+
useAdaptiveDensePlaneRouting &&
|
|
2820
|
+
bus.connections.length >= 8 &&
|
|
2821
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit) &&
|
|
2822
|
+
new Set(bus.routableEscapeLayers ?? bus.allowedLayers ?? []).size ===
|
|
2823
|
+
1 &&
|
|
2824
|
+
!matchedPlans.some((plan) =>
|
|
2825
|
+
wideBoundaryBuses.some(
|
|
2826
|
+
(candidate) => candidate.busId === plan.busId,
|
|
2827
|
+
),
|
|
2828
|
+
)
|
|
2829
|
+
) {
|
|
2830
|
+
const reservedVias = getReservedVias(bus)
|
|
2831
|
+
const siteRepairs = getSingleDogboneViaSiteRepairs(
|
|
2832
|
+
bus,
|
|
2833
|
+
{
|
|
2834
|
+
viaDiameter: this.config.viaDiameter,
|
|
2835
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
2836
|
+
traceWidth: this.config.traceWidth,
|
|
2837
|
+
clearance: this.config.clearance,
|
|
2838
|
+
additionalObstacles: this.routingSrj.obstacles,
|
|
2839
|
+
blockingSegments: [
|
|
2840
|
+
...matchedPlans.flatMap((plan) =>
|
|
2841
|
+
plan.segments.map((segment) => ({
|
|
2842
|
+
connectionIndex: plan.connectionIndex,
|
|
2843
|
+
segment,
|
|
2844
|
+
})),
|
|
2845
|
+
),
|
|
2846
|
+
...reservedVias.flatMap((reserved) =>
|
|
2847
|
+
reserved.sourceEscapeSegment
|
|
2848
|
+
? [
|
|
2849
|
+
{
|
|
2850
|
+
connectionIndex: -1,
|
|
2851
|
+
segment: reserved.sourceEscapeSegment,
|
|
2852
|
+
},
|
|
2853
|
+
]
|
|
2854
|
+
: [],
|
|
2855
|
+
),
|
|
2856
|
+
],
|
|
2857
|
+
blockingVias: [
|
|
2858
|
+
...matchedPlans.flatMap((plan) =>
|
|
2859
|
+
plan.via
|
|
2860
|
+
? [{ connectionIndex: plan.connectionIndex, ...plan.via }]
|
|
2861
|
+
: [],
|
|
2862
|
+
),
|
|
2863
|
+
...reservedVias.map((reserved) => ({
|
|
2864
|
+
connectionIndex: -1,
|
|
2865
|
+
...reserved.via,
|
|
2866
|
+
})),
|
|
2867
|
+
],
|
|
2868
|
+
},
|
|
2869
|
+
fixedViaPointsByConnectionIndex,
|
|
2870
|
+
)
|
|
2871
|
+
for (const replacementPoints of siteRepairs) {
|
|
2872
|
+
const replacementPlans = (yield* routeAlternatives(
|
|
2873
|
+
{
|
|
2874
|
+
...routeParams,
|
|
2875
|
+
fixedViaPointsByConnectionIndex: replacementPoints,
|
|
2876
|
+
reservedVias,
|
|
2877
|
+
fixedViaFallbackRouteOrderAttempts: 1,
|
|
2878
|
+
},
|
|
2879
|
+
1,
|
|
2880
|
+
))[0]
|
|
2881
|
+
if (!replacementPlans) continue
|
|
2882
|
+
busPlans = replacementPlans
|
|
2883
|
+
fixedViaPointsByConnectionIndex = replacementPoints
|
|
2884
|
+
usedRepairedViaSites = true
|
|
2885
|
+
break
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
// Fixed dogbones can close a turning bus's own escape channel. Retry
|
|
2889
|
+
// local sites while retaining every other connection's reservations.
|
|
2890
|
+
if (
|
|
2891
|
+
!busPlans &&
|
|
2892
|
+
useAdaptiveDensePlaneRouting &&
|
|
2893
|
+
bus.connections.length >= 8 &&
|
|
2894
|
+
getCornerBandSide(bus.exitEdge, bus.preferredExit)
|
|
2895
|
+
) {
|
|
2896
|
+
busPlans = (yield* routeAlternatives(
|
|
2897
|
+
{
|
|
2898
|
+
...routeParams,
|
|
2899
|
+
fixedViaPointsByConnectionIndex: undefined,
|
|
2900
|
+
reservedVias: getReservedVias(bus),
|
|
2901
|
+
},
|
|
2902
|
+
1,
|
|
2903
|
+
))[0]
|
|
2904
|
+
if (busPlans) {
|
|
2905
|
+
usedRepairedViaSites = true
|
|
2906
|
+
fixedViaPointsByConnectionIndex = new Map([
|
|
2907
|
+
...fixedViaPointsByConnectionIndex,
|
|
2908
|
+
...busPlans
|
|
2909
|
+
.filter((plan) => plan.via)
|
|
2910
|
+
.map(
|
|
2911
|
+
(plan) => [plan.connectionIndex, plan.via!.center] as const,
|
|
2912
|
+
),
|
|
2913
|
+
])
|
|
2914
|
+
}
|
|
2915
|
+
debugDense("local-sites", bus.busId, busPlans?.length ?? "failed")
|
|
2916
|
+
}
|
|
2917
|
+
// Retry a blocked wide bus with provisional plane sites as search
|
|
2918
|
+
// costs. A turning bus beside a centered field may also need that
|
|
2919
|
+
// neighboring field's uncommitted sites to move. Keep every committed
|
|
2920
|
+
// route and narrow reservation hard, then require a complete rematch.
|
|
2921
|
+
if (
|
|
2922
|
+
!busPlans &&
|
|
2923
|
+
bus.connections.length >= 8 &&
|
|
2924
|
+
(adjacentCenteredFieldByTurningBus.has(bus) ||
|
|
2925
|
+
boundaryBuses.some(
|
|
2926
|
+
(candidate) =>
|
|
2927
|
+
candidate.connections.length >= 8 &&
|
|
2928
|
+
matchedPlans.some((plan) => plan.busId === candidate.busId),
|
|
2929
|
+
))
|
|
2930
|
+
) {
|
|
2931
|
+
const committedNames = new Set(
|
|
2932
|
+
matchedPlans.map((plan) => plan.connectionName),
|
|
2933
|
+
)
|
|
2934
|
+
const provisionalReservationNames = new Set(
|
|
2935
|
+
this.preparedBuses
|
|
2936
|
+
.filter(
|
|
2937
|
+
(candidate) =>
|
|
2938
|
+
candidate.termination.type === "plane" ||
|
|
2939
|
+
candidate === adjacentCenteredFieldByTurningBus.get(bus),
|
|
2940
|
+
)
|
|
2941
|
+
.flatMap((candidate) =>
|
|
2942
|
+
candidate.connections.map(
|
|
2943
|
+
(connection) => connection.connection.name,
|
|
2944
|
+
),
|
|
2945
|
+
)
|
|
2946
|
+
.filter((name) => !committedNames.has(name)),
|
|
2947
|
+
)
|
|
2948
|
+
const freePlans = (yield* routeAlternatives(
|
|
2949
|
+
{
|
|
2950
|
+
...routeParams,
|
|
2951
|
+
fixedViaPointsByConnectionIndex: undefined,
|
|
2952
|
+
reservedVias: routeParams.reservedVias.filter(
|
|
2953
|
+
(reserved) =>
|
|
2954
|
+
!provisionalReservationNames.has(reserved.connectionName),
|
|
2955
|
+
),
|
|
2956
|
+
softReservedVias: routeParams.reservedVias.filter((reserved) =>
|
|
2957
|
+
provisionalReservationNames.has(reserved.connectionName),
|
|
2958
|
+
),
|
|
2959
|
+
},
|
|
2960
|
+
1,
|
|
2961
|
+
))[0]
|
|
2962
|
+
if (freePlans) {
|
|
2963
|
+
const allPlans = [...matchedPlans, ...freePlans]
|
|
2964
|
+
const rematchedPoints = matchComponentDogboneViaSites(
|
|
2965
|
+
this.preparedBuses,
|
|
2966
|
+
{
|
|
2967
|
+
viaDiameter: this.config.viaDiameter,
|
|
2968
|
+
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
2969
|
+
traceWidth: this.config.traceWidth,
|
|
2970
|
+
clearance: this.config.clearance,
|
|
2971
|
+
maximumSearchStates: 3_000_000,
|
|
2972
|
+
preferredBoundaryPerpendicularSideByBusId,
|
|
2973
|
+
preferBoundaryOutwardByBusId,
|
|
2974
|
+
fixedViaPointsByConnectionIndex: new Map(
|
|
2975
|
+
allPlans
|
|
2976
|
+
.filter((plan) => plan.via)
|
|
2977
|
+
.map((plan) => [plan.connectionIndex, plan.via!.center]),
|
|
2978
|
+
),
|
|
2979
|
+
preferredViaPointsByConnectionIndex:
|
|
2980
|
+
fixedViaPointsByConnectionIndex,
|
|
2981
|
+
blockingSegments: allPlans.flatMap((plan) =>
|
|
2982
|
+
plan.segments.map((segment) => ({
|
|
2983
|
+
connectionIndex: plan.connectionIndex,
|
|
2984
|
+
segment,
|
|
2985
|
+
})),
|
|
2986
|
+
),
|
|
2987
|
+
additionalObstacles: this.routingSrj.obstacles,
|
|
2988
|
+
preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
|
|
2989
|
+
canShareCopper,
|
|
2990
|
+
},
|
|
2991
|
+
)
|
|
2992
|
+
debugDense(
|
|
2993
|
+
"free-sites:rematched",
|
|
2994
|
+
bus.busId,
|
|
2995
|
+
rematchedPoints?.size ?? "failed",
|
|
2996
|
+
)
|
|
2997
|
+
if (rematchedPoints) {
|
|
2998
|
+
busPlans = freePlans
|
|
2999
|
+
fixedViaPointsByConnectionIndex = rematchedPoints
|
|
3000
|
+
usedRepairedViaSites = true
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
2420
3004
|
if (busPlans && bus.maxLengthSkew !== undefined) {
|
|
2421
3005
|
const lengths = busPlans.map((plan) => plan.length)
|
|
2422
3006
|
const rawSkew = Math.max(...lengths) - Math.min(...lengths)
|
|
@@ -2430,7 +3014,13 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2430
3014
|
// Only pay for additional A* variants when the first topology is so
|
|
2431
3015
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2432
3016
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2433
|
-
|
|
3017
|
+
// Keep repaired via sites: routeParams still carries the
|
|
3018
|
+
// earlier provisional sites and cannot safely replace its geometry.
|
|
3019
|
+
if (
|
|
3020
|
+
needsRouteDiversity &&
|
|
3021
|
+
!matchLengthsAfterPlanes &&
|
|
3022
|
+
!usedRepairedViaSites
|
|
3023
|
+
) {
|
|
2434
3024
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2435
3025
|
(first, second) => {
|
|
2436
3026
|
const firstLengths = first.map((plan) => plan.length)
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -338,9 +338,33 @@ function replacementCopperIsSelfClear(params: {
|
|
|
338
338
|
) {
|
|
339
339
|
continue
|
|
340
340
|
}
|
|
341
|
+
const viaClearance = via.diameter / 2 + replacement.width / 2 + clearance
|
|
342
|
+
// An off-grid via can join this run through a short, connected stub.
|
|
343
|
+
// Preserve that existing connection while rejecting later approaches.
|
|
344
|
+
const connectsThroughShortStub = (direction: -1 | 1): boolean => {
|
|
345
|
+
let point = direction === -1 ? replacement.start : replacement.end
|
|
346
|
+
let pathDistance = 0
|
|
347
|
+
for (
|
|
348
|
+
let index = replacementIndex + direction;
|
|
349
|
+
index >= 0 && index < segments.length;
|
|
350
|
+
index += direction
|
|
351
|
+
) {
|
|
352
|
+
const segment = segments[index]!
|
|
353
|
+
if (segment.layer !== replacement.layer) return false
|
|
354
|
+
const connectedEnd = direction === -1 ? segment.end : segment.start
|
|
355
|
+
if (!pointsMatch(point, connectedEnd)) return false
|
|
356
|
+
pathDistance += distance(segment.start, segment.end)
|
|
357
|
+
if (pathDistance > viaClearance + EPSILON) return false
|
|
358
|
+
point = direction === -1 ? segment.start : segment.end
|
|
359
|
+
if (pointsMatch(point, via.center)) return true
|
|
360
|
+
}
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
341
363
|
if (
|
|
342
364
|
distancePointToSegment(via.center, replacement.start, replacement.end) <
|
|
343
|
-
|
|
365
|
+
viaClearance - EPSILON &&
|
|
366
|
+
!connectsThroughShortStub(-1) &&
|
|
367
|
+
!connectsThroughShortStub(1)
|
|
344
368
|
) {
|
|
345
369
|
return false
|
|
346
370
|
}
|
|
@@ -782,3 +782,30 @@ export function getComponentDogboneViaSiteCandidates(
|
|
|
782
782
|
),
|
|
783
783
|
)
|
|
784
784
|
}
|
|
785
|
+
|
|
786
|
+
/** Try one adjacent site change without releasing any other fixed via. */
|
|
787
|
+
export function* getSingleDogboneViaSiteRepairs(
|
|
788
|
+
bus: PreparedBus,
|
|
789
|
+
rules: DogboneViaSiteGeometryRules,
|
|
790
|
+
fixedViaPointsByConnectionIndex: ReadonlyMap<number, Point2D>,
|
|
791
|
+
): Generator<ReadonlyMap<number, Point2D>, void, unknown> {
|
|
792
|
+
const sites = getComponentDogboneViaSiteCandidates([bus], rules)
|
|
793
|
+
let remainingAttempts = 24
|
|
794
|
+
for (const connection of bus.connections.toReversed()) {
|
|
795
|
+
const original = fixedViaPointsByConnectionIndex.get(
|
|
796
|
+
connection.connectionIndex,
|
|
797
|
+
)
|
|
798
|
+
if (!original) continue
|
|
799
|
+
for (const candidate of sites) {
|
|
800
|
+
if (
|
|
801
|
+
candidate.connectionIndex !== connection.connectionIndex ||
|
|
802
|
+
distance(candidate.point, original) <= EPSILON
|
|
803
|
+
)
|
|
804
|
+
continue
|
|
805
|
+
if (remainingAttempts-- <= 0) return
|
|
806
|
+
const replacement = new Map(fixedViaPointsByConnectionIndex)
|
|
807
|
+
replacement.set(connection.connectionIndex, candidate.point)
|
|
808
|
+
yield replacement
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
package/lib/route-bus.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface RouteBusParams {
|
|
|
62
62
|
stopAfterFirstRejectedViaMinimalCandidate?: boolean
|
|
63
63
|
fixedViaPointsByConnectionIndex?: ReadonlyMap<number, Point2D>
|
|
64
64
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
|
+
/** Provisional site preferences; successful callers must rematch future vias. */
|
|
66
|
+
softReservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
67
|
viaMinimalOnly?: boolean
|
|
66
68
|
/** Permit a singleton or pair to move provisional vias near the boundary. */
|
|
67
69
|
allowBoundarySideViaFallback?: boolean
|
|
@@ -163,6 +165,7 @@ function getWindingTargetOrders(params: {
|
|
|
163
165
|
}): {
|
|
164
166
|
orders: PreparedConnection[][]
|
|
165
167
|
legacyOrder: PreparedConnection[]
|
|
168
|
+
ordinaryOrderCount: number
|
|
166
169
|
} {
|
|
167
170
|
const { bus, boundaryDirection, layerNames, targetLayer } = params
|
|
168
171
|
const getTargetLayer = (candidate: PreparedConnection): string =>
|
|
@@ -266,6 +269,41 @@ function getWindingTargetOrders(params: {
|
|
|
266
269
|
// but do not let sub-nanometer noise between unrelated layer bands choose
|
|
267
270
|
// the primary topology.
|
|
268
271
|
candidateOrders.push(legacyOrderedConnections)
|
|
272
|
+
const ordinaryOrderCount = new Set(
|
|
273
|
+
candidateOrders.map((order) =>
|
|
274
|
+
order.map((candidate) => candidate.connectionIndex).join(","),
|
|
275
|
+
),
|
|
276
|
+
).size
|
|
277
|
+
// Preserve each original layer's lane order while exploring other legal
|
|
278
|
+
// interleavings. Keep this bounded for buses with many source layers.
|
|
279
|
+
if (
|
|
280
|
+
!getCornerSide(bus) &&
|
|
281
|
+
bus.connections.length <= 8 &&
|
|
282
|
+
orderedLayers.length > 1
|
|
283
|
+
) {
|
|
284
|
+
const layerSequences = orderedLayers.map((layer) =>
|
|
285
|
+
connectionsByLayer.get(layer)!.toSorted(compareWithinLayer),
|
|
286
|
+
)
|
|
287
|
+
const offsets = layerSequences.map(() => 0)
|
|
288
|
+
const current: PreparedConnection[] = []
|
|
289
|
+
const append = (): void => {
|
|
290
|
+
if (candidateOrders.length >= 128) return
|
|
291
|
+
if (current.length === bus.connections.length) {
|
|
292
|
+
candidateOrders.push([...current])
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
for (let layer = 0; layer < layerSequences.length; layer++) {
|
|
296
|
+
const next = layerSequences[layer]![offsets[layer]!]
|
|
297
|
+
if (!next) continue
|
|
298
|
+
offsets[layer]++
|
|
299
|
+
current.push(next)
|
|
300
|
+
append()
|
|
301
|
+
current.pop()
|
|
302
|
+
offsets[layer]--
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
append()
|
|
306
|
+
}
|
|
269
307
|
const seenOrders = new Set<string>()
|
|
270
308
|
const orders = candidateOrders.filter((order) => {
|
|
271
309
|
const key = order.map((candidate) => candidate.connectionIndex).join(",")
|
|
@@ -273,7 +311,7 @@ function getWindingTargetOrders(params: {
|
|
|
273
311
|
seenOrders.add(key)
|
|
274
312
|
return true
|
|
275
313
|
})
|
|
276
|
-
return { orders, legacyOrder: legacyOrderedConnections }
|
|
314
|
+
return { orders, legacyOrder: legacyOrderedConnections, ordinaryOrderCount }
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
function getWindingTargetRank(params: {
|
|
@@ -311,10 +349,92 @@ function getWindingCrossoverLayer(params: {
|
|
|
311
349
|
)
|
|
312
350
|
}
|
|
313
351
|
|
|
352
|
+
function getDistributedBoundaryTargetTracks(params: {
|
|
353
|
+
bus: PreparedBus
|
|
354
|
+
boundaryDirection: FanoutDirection
|
|
355
|
+
traceWidth: number
|
|
356
|
+
clearance: number
|
|
357
|
+
allowLayerInterleaving?: boolean
|
|
358
|
+
}): number[] | undefined {
|
|
359
|
+
const { bus, boundaryDirection, traceWidth, clearance } = params
|
|
360
|
+
if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
|
|
361
|
+
return undefined
|
|
362
|
+
const layers = new Set(
|
|
363
|
+
bus.connections.map(
|
|
364
|
+
(connection) =>
|
|
365
|
+
connection.exitTargetPoint?.layer ??
|
|
366
|
+
getPointLayer(connection.targetPoint),
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
if (layers.size < 2) return undefined
|
|
370
|
+
const minimum = isHorizontal(boundaryDirection)
|
|
371
|
+
? bus.sharedBoundary.minY
|
|
372
|
+
: bus.sharedBoundary.minX
|
|
373
|
+
const maximum = isHorizontal(boundaryDirection)
|
|
374
|
+
? bus.sharedBoundary.maxY
|
|
375
|
+
: bus.sharedBoundary.maxX
|
|
376
|
+
const tracks = bus.connections
|
|
377
|
+
.map((connection) =>
|
|
378
|
+
Math.max(
|
|
379
|
+
minimum,
|
|
380
|
+
Math.min(
|
|
381
|
+
maximum,
|
|
382
|
+
getPerpendicularAxis(
|
|
383
|
+
connection.exitTargetPoint ?? connection.targetPoint,
|
|
384
|
+
boundaryDirection,
|
|
385
|
+
),
|
|
386
|
+
),
|
|
387
|
+
),
|
|
388
|
+
)
|
|
389
|
+
.toSorted((a, b) => a - b)
|
|
390
|
+
const pitch = traceWidth + clearance
|
|
391
|
+
if (
|
|
392
|
+
!params.allowLayerInterleaving &&
|
|
393
|
+
tracks.every(
|
|
394
|
+
(track, index) =>
|
|
395
|
+
index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
return undefined
|
|
399
|
+
if (maximum - minimum < (tracks.length - 1) * pitch - 1e-9) return undefined
|
|
400
|
+
// Pool neighboring overlaps while preserving their mean requested location.
|
|
401
|
+
// Subtracting the pitch reduces the clearance constraint to monotonicity.
|
|
402
|
+
const blocks: { start: number; count: number; mean: number }[] = []
|
|
403
|
+
for (const [index, track] of tracks.entries()) {
|
|
404
|
+
blocks.push({ start: index, count: 1, mean: track - index * pitch })
|
|
405
|
+
while (blocks.length > 1 && blocks.at(-2)!.mean > blocks.at(-1)!.mean) {
|
|
406
|
+
const second = blocks.pop()!
|
|
407
|
+
const first = blocks.pop()!
|
|
408
|
+
blocks.push({
|
|
409
|
+
start: first.start,
|
|
410
|
+
count: first.count + second.count,
|
|
411
|
+
mean:
|
|
412
|
+
(first.mean * first.count + second.mean * second.count) /
|
|
413
|
+
(first.count + second.count),
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const block of blocks) {
|
|
418
|
+
const mean = Math.max(
|
|
419
|
+
minimum,
|
|
420
|
+
Math.min(maximum - (tracks.length - 1) * pitch, block.mean),
|
|
421
|
+
)
|
|
422
|
+
for (let index = block.start; index < block.start + block.count; index++)
|
|
423
|
+
tracks[index] = mean + index * pitch
|
|
424
|
+
}
|
|
425
|
+
return tracks
|
|
426
|
+
}
|
|
427
|
+
|
|
314
428
|
export function getBoundaryTargetTrack(params: {
|
|
315
429
|
bus: PreparedBus
|
|
316
430
|
connection: PreparedConnection
|
|
317
431
|
boundaryDirection: FanoutDirection
|
|
432
|
+
traceWidth?: number
|
|
433
|
+
clearance?: number
|
|
434
|
+
layerNames?: readonly string[]
|
|
435
|
+
targetLayer?: string
|
|
436
|
+
windingOrderIndex?: number
|
|
437
|
+
allowLayerInterleaving?: boolean
|
|
318
438
|
}): number {
|
|
319
439
|
const requestedTrack = getPerpendicularAxis(
|
|
320
440
|
params.connection.exitTargetPoint ?? params.connection.targetPoint,
|
|
@@ -326,6 +446,22 @@ export function getBoundaryTargetTrack(params: {
|
|
|
326
446
|
const boundaryMaximum = isHorizontal(params.boundaryDirection)
|
|
327
447
|
? params.bus.sharedBoundary.maxY
|
|
328
448
|
: params.bus.sharedBoundary.maxX
|
|
449
|
+
const distributedTracks =
|
|
450
|
+
params.traceWidth !== undefined && params.clearance !== undefined
|
|
451
|
+
? getDistributedBoundaryTargetTracks({
|
|
452
|
+
...params,
|
|
453
|
+
traceWidth: params.traceWidth,
|
|
454
|
+
clearance: params.clearance,
|
|
455
|
+
})
|
|
456
|
+
: undefined
|
|
457
|
+
if (distributedTracks && params.layerNames && params.targetLayer) {
|
|
458
|
+
const { rank } = getWindingTargetRank({
|
|
459
|
+
...params,
|
|
460
|
+
layerNames: params.layerNames,
|
|
461
|
+
targetLayer: params.targetLayer,
|
|
462
|
+
})
|
|
463
|
+
return distributedTracks[rank]!
|
|
464
|
+
}
|
|
329
465
|
return Math.max(boundaryMinimum, Math.min(boundaryMaximum, requestedTrack))
|
|
330
466
|
}
|
|
331
467
|
|
|
@@ -1102,6 +1238,10 @@ function buildPlan(params: {
|
|
|
1102
1238
|
bus,
|
|
1103
1239
|
connection: preparedConnection,
|
|
1104
1240
|
boundaryDirection,
|
|
1241
|
+
traceWidth,
|
|
1242
|
+
clearance,
|
|
1243
|
+
layerNames,
|
|
1244
|
+
targetLayer,
|
|
1105
1245
|
})
|
|
1106
1246
|
: track
|
|
1107
1247
|
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
@@ -2459,6 +2599,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2459
2599
|
stopAfterFirstRejectedViaMinimalCandidate = false,
|
|
2460
2600
|
fixedViaPointsByConnectionIndex,
|
|
2461
2601
|
reservedVias = [],
|
|
2602
|
+
softReservedVias = [],
|
|
2462
2603
|
viaMinimalOnly = false,
|
|
2463
2604
|
allowBoundarySideViaFallback = false,
|
|
2464
2605
|
preferCornerBoundaryVia = false,
|
|
@@ -2606,15 +2747,29 @@ export function* routeBusAlternativesSteps(
|
|
|
2606
2747
|
windingOrderIndex?: number
|
|
2607
2748
|
preferTargetDirectedLaneBias?: boolean
|
|
2608
2749
|
localDogboneRepair?: boolean
|
|
2750
|
+
reserveTerminalExitPoints?: boolean
|
|
2751
|
+
cornerExitLaneOffset?: number
|
|
2752
|
+
allowLayerInterleaving?: boolean
|
|
2609
2753
|
}
|
|
2610
2754
|
const maximumThroughAllRouteOrderAttempts = 24
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2755
|
+
const usesDistributedWindingTargets = Boolean(
|
|
2756
|
+
cornerSide ||
|
|
2757
|
+
getDistributedBoundaryTargetTracks({
|
|
2613
2758
|
bus,
|
|
2614
2759
|
boundaryDirection,
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
})
|
|
2760
|
+
traceWidth,
|
|
2761
|
+
clearance,
|
|
2762
|
+
}),
|
|
2763
|
+
)
|
|
2764
|
+
const windingTargetOrders = getWindingTargetOrders({
|
|
2765
|
+
bus,
|
|
2766
|
+
boundaryDirection,
|
|
2767
|
+
layerNames,
|
|
2768
|
+
targetLayer,
|
|
2769
|
+
})
|
|
2770
|
+
const windingTargetOrderCount = windingTargetOrders.orders.length
|
|
2771
|
+
const ordinaryWindingTargetOrderCount = usesDistributedWindingTargets
|
|
2772
|
+
? windingTargetOrders.ordinaryOrderCount
|
|
2618
2773
|
: 1
|
|
2619
2774
|
const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2620
2775
|
viaHandednesses.map((viaHandedness) => ({
|
|
@@ -2760,7 +2915,13 @@ export function* routeBusAlternativesSteps(
|
|
|
2760
2915
|
const coordinatedViaPoints =
|
|
2761
2916
|
fixedViaPointsByConnectionIndex ??
|
|
2762
2917
|
(!allowBlindAndBuriedVias &&
|
|
2763
|
-
bus.connections.length >= 8
|
|
2918
|
+
(bus.connections.length >= 8 ||
|
|
2919
|
+
getDistributedBoundaryTargetTracks({
|
|
2920
|
+
bus,
|
|
2921
|
+
boundaryDirection,
|
|
2922
|
+
traceWidth,
|
|
2923
|
+
clearance,
|
|
2924
|
+
})) &&
|
|
2764
2925
|
reservedVias.length === 0
|
|
2765
2926
|
? matchComponentDogboneViaSites([bus], {
|
|
2766
2927
|
viaDiameter,
|
|
@@ -2789,7 +2950,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2789
2950
|
coordinatedViaPoints
|
|
2790
2951
|
? [
|
|
2791
2952
|
...Array.from(
|
|
2792
|
-
{ length:
|
|
2953
|
+
{ length: ordinaryWindingTargetOrderCount },
|
|
2793
2954
|
(_, windingOrderIndex) => ({
|
|
2794
2955
|
label: `component-matched-vias-winding-${windingOrderIndex}`,
|
|
2795
2956
|
useViaInPad: false,
|
|
@@ -2813,6 +2974,26 @@ export function* routeBusAlternativesSteps(
|
|
|
2813
2974
|
windingOrderIndex: 0,
|
|
2814
2975
|
preferTargetDirectedLaneBias: true,
|
|
2815
2976
|
},
|
|
2977
|
+
// Keep ordinary fixed-site attempts first. Forward retries reserve
|
|
2978
|
+
// future exits so an earlier lane cannot close their final gap.
|
|
2979
|
+
...(!getCornerSide(bus) && windingTargetOrderCount > 1
|
|
2980
|
+
? Array.from(
|
|
2981
|
+
{ length: windingTargetOrderCount },
|
|
2982
|
+
(_, windingOrderIndex) =>
|
|
2983
|
+
[true, false].map((preferTargetDirectedLaneBias) => ({
|
|
2984
|
+
label: `expanded-winding-${windingOrderIndex}-${preferTargetDirectedLaneBias}`,
|
|
2985
|
+
useViaInPad: false,
|
|
2986
|
+
getViaHandedness: () => 0 as const,
|
|
2987
|
+
getViaPoint: (connection: PreparedConnection) =>
|
|
2988
|
+
coordinatedViaPoints.get(connection.connectionIndex)!,
|
|
2989
|
+
maximumRouteOrderAttempts: 1,
|
|
2990
|
+
windingOrderIndex,
|
|
2991
|
+
preferTargetDirectedLaneBias,
|
|
2992
|
+
reserveTerminalExitPoints: !preferTargetDirectedLaneBias,
|
|
2993
|
+
allowLayerInterleaving: true,
|
|
2994
|
+
})),
|
|
2995
|
+
).flat()
|
|
2996
|
+
: []),
|
|
2816
2997
|
]
|
|
2817
2998
|
: []
|
|
2818
2999
|
const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
|
|
@@ -2880,6 +3061,23 @@ export function* routeBusAlternativesSteps(
|
|
|
2880
3061
|
}
|
|
2881
3062
|
}
|
|
2882
3063
|
}
|
|
3064
|
+
if (alignWindingGridToPads && cornerSide) {
|
|
3065
|
+
const layerLocalExitOffset = getCornerLaneOffsets(
|
|
3066
|
+
bus,
|
|
3067
|
+
acceptedPlans.filter((plan) => plan.targetLayer === targetLayer),
|
|
3068
|
+
).exit
|
|
3069
|
+
if (layerLocalExitOffset !== cornerLaneOffsets.exit) {
|
|
3070
|
+
// Preserve successful shared-band routes first. If those patterns
|
|
3071
|
+
// fail, reuse the corner slots occupied only on other copper layers.
|
|
3072
|
+
terminalPatterns.push(
|
|
3073
|
+
...terminalPatterns.map((pattern) => ({
|
|
3074
|
+
...pattern,
|
|
3075
|
+
label: `${pattern.label}-layer-local-corner`,
|
|
3076
|
+
cornerExitLaneOffset: layerLocalExitOffset,
|
|
3077
|
+
})),
|
|
3078
|
+
)
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
2883
3081
|
const seenTerminalSignatures = new Set<string>()
|
|
2884
3082
|
for (const terminalPattern of terminalPatterns) {
|
|
2885
3083
|
const terminals = bus.connections.map((preparedConnection) => {
|
|
@@ -2889,7 +3087,8 @@ export function* routeBusAlternativesSteps(
|
|
|
2889
3087
|
? getCornerTargetTrack({
|
|
2890
3088
|
bus,
|
|
2891
3089
|
connection: preparedConnection,
|
|
2892
|
-
cornerExitLaneOffset:
|
|
3090
|
+
cornerExitLaneOffset:
|
|
3091
|
+
terminalPattern.cornerExitLaneOffset ?? cornerLaneOffsets.exit,
|
|
2893
3092
|
traceWidth,
|
|
2894
3093
|
viaDiameter,
|
|
2895
3094
|
clearance,
|
|
@@ -2899,9 +3098,15 @@ export function* routeBusAlternativesSteps(
|
|
|
2899
3098
|
cornerBandTargetTrackOffset,
|
|
2900
3099
|
})
|
|
2901
3100
|
: getBoundaryTargetTrack({
|
|
3101
|
+
allowLayerInterleaving: terminalPattern.allowLayerInterleaving,
|
|
2902
3102
|
bus,
|
|
2903
3103
|
connection: preparedConnection,
|
|
2904
3104
|
boundaryDirection,
|
|
3105
|
+
traceWidth,
|
|
3106
|
+
clearance,
|
|
3107
|
+
layerNames,
|
|
3108
|
+
targetLayer,
|
|
3109
|
+
windingOrderIndex: terminalPattern.windingOrderIndex,
|
|
2905
3110
|
})
|
|
2906
3111
|
return {
|
|
2907
3112
|
connection: preparedConnection,
|
|
@@ -2981,7 +3186,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2981
3186
|
)
|
|
2982
3187
|
.join(
|
|
2983
3188
|
"|",
|
|
2984
|
-
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}`
|
|
3189
|
+
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}:${Boolean(terminalPattern.preferTargetDirectedLaneBias)}:${Boolean(terminalPattern.reserveTerminalExitPoints)}`
|
|
2985
3190
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
2986
3191
|
seenTerminalSignatures.add(terminalSignature)
|
|
2987
3192
|
const windingSteps = routeViaMinimalWindingAlternativesSteps(
|
|
@@ -3003,6 +3208,8 @@ export function* routeBusAlternativesSteps(
|
|
|
3003
3208
|
alignGridToPads,
|
|
3004
3209
|
includeReverseTargetRotation: terminalPattern.localDogboneRepair,
|
|
3005
3210
|
reservedVias,
|
|
3211
|
+
softReservedVias,
|
|
3212
|
+
reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
|
|
3006
3213
|
gridStepDivisor,
|
|
3007
3214
|
preferTargetDirectedLaneBias:
|
|
3008
3215
|
terminalPattern.preferTargetDirectedLaneBias,
|
|
@@ -3095,6 +3302,10 @@ export function* routeBusAlternativesSteps(
|
|
|
3095
3302
|
bus,
|
|
3096
3303
|
connection: preparedConnection,
|
|
3097
3304
|
boundaryDirection,
|
|
3305
|
+
traceWidth,
|
|
3306
|
+
clearance,
|
|
3307
|
+
layerNames,
|
|
3308
|
+
targetLayer,
|
|
3098
3309
|
}),
|
|
3099
3310
|
)
|
|
3100
3311
|
const finalExitPoints = finalTracks.map((track) =>
|
|
@@ -3182,47 +3393,64 @@ export function* routeBusAlternativesSteps(
|
|
|
3182
3393
|
),
|
|
3183
3394
|
)
|
|
3184
3395
|
: []
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3396
|
+
const matchedVias = bus.connections.map(
|
|
3397
|
+
(connection) =>
|
|
3398
|
+
fixedViaPointsByConnectionIndex.get(connection.connectionIndex)!,
|
|
3399
|
+
)
|
|
3400
|
+
const packageEdgeViaCandidates = [
|
|
3401
|
+
{
|
|
3402
|
+
distance: sourceCenter.x - bus.componentBounds.minX,
|
|
3403
|
+
axis: "x" as const,
|
|
3404
|
+
value: bus.componentBounds.minX - 2 * insetStep,
|
|
3405
|
+
},
|
|
3406
|
+
{
|
|
3407
|
+
distance: bus.componentBounds.maxX - sourceCenter.x,
|
|
3408
|
+
axis: "x" as const,
|
|
3409
|
+
value: bus.componentBounds.maxX + 2 * insetStep,
|
|
3410
|
+
},
|
|
3411
|
+
{
|
|
3412
|
+
distance: sourceCenter.y - bus.componentBounds.minY,
|
|
3413
|
+
axis: "y" as const,
|
|
3414
|
+
value: bus.componentBounds.minY - 2 * insetStep,
|
|
3415
|
+
},
|
|
3416
|
+
{
|
|
3417
|
+
distance: bus.componentBounds.maxY - sourceCenter.y,
|
|
3418
|
+
axis: "y" as const,
|
|
3419
|
+
value: bus.componentBounds.maxY + 2 * insetStep,
|
|
3420
|
+
},
|
|
3421
|
+
]
|
|
3422
|
+
.toSorted((a, b) => a.distance - b.distance)
|
|
3423
|
+
.flatMap(({ axis, value }) => {
|
|
3424
|
+
const otherAxis = axis === "x" ? "y" : "x"
|
|
3425
|
+
const mean =
|
|
3426
|
+
matchedVias.reduce((sum, via) => sum + via[otherAxis], 0) /
|
|
3427
|
+
matchedVias.length
|
|
3428
|
+
const order = matchedVias
|
|
3429
|
+
.map((via, index) => ({ index, track: via[otherAxis] }))
|
|
3430
|
+
.toSorted((a, b) => a.track - b.track || a.index - b.index)
|
|
3431
|
+
const pitch = viaDiameter + clearance
|
|
3432
|
+
const points = matchedVias.map((via, index) => ({
|
|
3433
|
+
...via,
|
|
3434
|
+
[axis]: value,
|
|
3435
|
+
[otherAxis]:
|
|
3436
|
+
order.length === 2 &&
|
|
3437
|
+
Math.abs(order[1]!.track - order[0]!.track) < pitch
|
|
3438
|
+
? mean + (order.findIndex((v) => v.index === index) - 0.5) * pitch
|
|
3439
|
+
: via[otherAxis],
|
|
3440
|
+
}))
|
|
3441
|
+
return points.length === 2
|
|
3442
|
+
? [points, [points[1]!, points[0]!]]
|
|
3443
|
+
: [points]
|
|
3444
|
+
})
|
|
3445
|
+
const preferPackageEdgeVias =
|
|
3446
|
+
bus.connections.length === 2 && Boolean(getCornerSide(bus))
|
|
3225
3447
|
const viaCandidates = [
|
|
3448
|
+
...(preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3449
|
+
(points) => ({
|
|
3450
|
+
points,
|
|
3451
|
+
boundarySide: false,
|
|
3452
|
+
}),
|
|
3453
|
+
),
|
|
3226
3454
|
...displacedViaCandidates.map((points) => ({
|
|
3227
3455
|
points,
|
|
3228
3456
|
boundarySide: false,
|
|
@@ -3231,13 +3459,15 @@ export function* routeBusAlternativesSteps(
|
|
|
3231
3459
|
points,
|
|
3232
3460
|
boundarySide: true,
|
|
3233
3461
|
})),
|
|
3234
|
-
// Preserve existing singleton escapes before trying a short
|
|
3235
|
-
// route beyond the package. The target layer can then wind to the exit
|
|
3462
|
+
// Preserve existing centered and singleton escapes before trying a short
|
|
3463
|
+
// source-layer route beyond the package. The target layer can then wind to the exit
|
|
3236
3464
|
// without a local via being fenced in by an already-routed wide bus.
|
|
3237
|
-
...packageEdgeViaCandidates.map(
|
|
3238
|
-
points
|
|
3239
|
-
|
|
3240
|
-
|
|
3465
|
+
...(!preferPackageEdgeVias ? packageEdgeViaCandidates : []).map(
|
|
3466
|
+
(points) => ({
|
|
3467
|
+
points,
|
|
3468
|
+
boundarySide: false,
|
|
3469
|
+
}),
|
|
3470
|
+
),
|
|
3241
3471
|
]
|
|
3242
3472
|
for (const { points: boundaryViaPoints, boundarySide } of viaCandidates) {
|
|
3243
3473
|
const boundaryVias = bus.connections.map((connection, index) => ({
|
|
@@ -3301,6 +3531,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3301
3531
|
allowBlindAndBuriedVias,
|
|
3302
3532
|
allowSameNetMerges,
|
|
3303
3533
|
maximumRouteOrderAttempts: bus.connections.length === 1 ? 3 : 6,
|
|
3534
|
+
softReservedVias,
|
|
3304
3535
|
reservedVias:
|
|
3305
3536
|
bus.connections.length > 1
|
|
3306
3537
|
? [...reservedVias, ...boundaryVias]
|
|
@@ -3350,6 +3581,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3350
3581
|
allowSameNetMerges,
|
|
3351
3582
|
maximumRouteOrderAttempts: 6,
|
|
3352
3583
|
reservedVias,
|
|
3584
|
+
softReservedVias,
|
|
3353
3585
|
gridStepDivisor: 2,
|
|
3354
3586
|
alignGridToPads: true,
|
|
3355
3587
|
},
|
|
@@ -62,6 +62,8 @@ export interface RouteViaMinimalWindingParams {
|
|
|
62
62
|
allowSameNetMerges?: boolean
|
|
63
63
|
maximumRouteOrderAttempts?: number
|
|
64
64
|
reservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
|
+
/** Cost hints for provisional sites that the caller must rematch before commit. */
|
|
66
|
+
softReservedVias?: readonly ViaMinimalWindingReservedVia[]
|
|
65
67
|
/** Use a finer uniform grid for narrow channels between reserved vias. */
|
|
66
68
|
gridStepDivisor?: 1 | 2
|
|
67
69
|
/** Bias bounded fixed-site searches toward the remote target band. */
|
|
@@ -74,6 +76,8 @@ export interface RouteViaMinimalWindingParams {
|
|
|
74
76
|
alignGridToPads?: boolean
|
|
75
77
|
/** Defer the outermost reversed target while routing the inner terminals. */
|
|
76
78
|
includeReverseTargetRotation?: boolean
|
|
79
|
+
/** Keep earlier lanes clear of every remaining terminal exit. */
|
|
80
|
+
reserveTerminalExitPoints?: boolean
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
export interface RouteViaMinimalWindingProgress {
|
|
@@ -637,12 +641,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
637
641
|
allowSameNetMerges = false,
|
|
638
642
|
maximumRouteOrderAttempts,
|
|
639
643
|
reservedVias = [],
|
|
644
|
+
softReservedVias = [],
|
|
640
645
|
gridStepDivisor = 1,
|
|
641
646
|
preferTargetDirectedLaneBias = false,
|
|
642
647
|
allowSourceLayerRouting = false,
|
|
643
648
|
adaptiveRouteOrder = false,
|
|
644
649
|
alignGridToPads = false,
|
|
645
650
|
includeReverseTargetRotation = false,
|
|
651
|
+
reserveTerminalExitPoints = false,
|
|
646
652
|
} = params
|
|
647
653
|
if (
|
|
648
654
|
maximumRouteOrderAttempts !== undefined &&
|
|
@@ -702,6 +708,40 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
702
708
|
point: { x: gridMinX + column * gridStep, y: gridMinY + row * gridStep },
|
|
703
709
|
}
|
|
704
710
|
})
|
|
711
|
+
// Provisional plane barrels guide search without being fixed obstacles.
|
|
712
|
+
// Touch only the small grid rectangles around each disk, not every node.
|
|
713
|
+
const softViaCosts = softReservedVias.length
|
|
714
|
+
? new Float32Array(nodeCount)
|
|
715
|
+
: undefined
|
|
716
|
+
if (softViaCosts) {
|
|
717
|
+
for (const { via } of softReservedVias) {
|
|
718
|
+
if (!via.spanLayers.includes(targetLayer)) continue
|
|
719
|
+
const radius = via.diameter / 2 + traceWidth / 2 + clearance
|
|
720
|
+
const minimumColumn = Math.max(
|
|
721
|
+
0,
|
|
722
|
+
Math.ceil((via.center.x - radius - gridMinX) / gridStep),
|
|
723
|
+
)
|
|
724
|
+
const maximumColumn = Math.min(
|
|
725
|
+
columnCount - 1,
|
|
726
|
+
Math.floor((via.center.x + radius - gridMinX) / gridStep),
|
|
727
|
+
)
|
|
728
|
+
const minimumRow = Math.max(
|
|
729
|
+
0,
|
|
730
|
+
Math.ceil((via.center.y - radius - gridMinY) / gridStep),
|
|
731
|
+
)
|
|
732
|
+
const maximumRow = Math.min(
|
|
733
|
+
rowCount - 1,
|
|
734
|
+
Math.floor((via.center.y + radius - gridMinY) / gridStep),
|
|
735
|
+
)
|
|
736
|
+
for (let row = minimumRow; row <= maximumRow; row++) {
|
|
737
|
+
for (let column = minimumColumn; column <= maximumColumn; column++) {
|
|
738
|
+
const index = row * columnCount + column
|
|
739
|
+
if (distance(nodes[index]!.point, via.center) < radius)
|
|
740
|
+
softViaCosts[index] = softViaCosts[index]! + 25 * gridStep
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
705
745
|
const sampledGridPoints = includeVisualization
|
|
706
746
|
? nodes
|
|
707
747
|
.filter(
|
|
@@ -869,6 +909,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
869
909
|
return false
|
|
870
910
|
}
|
|
871
911
|
}
|
|
912
|
+
for (const other of reserveTerminalExitPoints ? terminals : []) {
|
|
913
|
+
if (sharesNet(connectionName, other.connection.connection.name)) continue
|
|
914
|
+
if (
|
|
915
|
+
distancePointToSegment(other.exitPoint, segment.start, segment.end) <
|
|
916
|
+
traceWidth + clearance - EPSILON
|
|
917
|
+
)
|
|
918
|
+
return false
|
|
919
|
+
}
|
|
872
920
|
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
873
921
|
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
874
922
|
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|
|
@@ -1247,7 +1295,8 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
1247
1295
|
? gridStep * Math.SQRT2
|
|
1248
1296
|
: gridStep) +
|
|
1249
1297
|
(addsTurn ? gridStep * 0.2 : 0) +
|
|
1250
|
-
lanePenalty
|
|
1298
|
+
lanePenalty +
|
|
1299
|
+
(softViaCosts?.[nextNode] ?? 0)
|
|
1251
1300
|
const nextState = nextNode * 9 + directionIndex
|
|
1252
1301
|
if (nextDistance >= distances[nextState]! - EPSILON) continue
|
|
1253
1302
|
const edgeIndex = current.node * 8 + directionIndex
|