@tscircuit/fanout-solver 0.0.60 → 0.0.62
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/README.md +22 -10
- package/lib/fanout-solver.ts +388 -21
- package/lib/match-bus-lengths.ts +25 -1
- package/lib/match-component-dogbone-via-sites.ts +27 -0
- package/lib/route-bus.ts +104 -18
- package/lib/route-via-minimal-winding.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,8 +282,8 @@ extents.
|
|
|
282
282
|
|
|
283
283
|
## Dataset 31 benchmark
|
|
284
284
|
|
|
285
|
-
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark **
|
|
286
|
-
directional cases** from
|
|
285
|
+
Run `./benchmark.sh` (or `bun run benchmark`) to benchmark **all 24 dataset 31
|
|
286
|
+
directional cases: 12 AM62L and 12 RK3308** from
|
|
287
287
|
[`tscircuit/dataset-fanout31-am62l`](https://github.com/tscircuit/dataset-fanout31-am62l).
|
|
288
288
|
The upstream revision is pinned in `scripts/generate-repro/package.json` and
|
|
289
289
|
recorded in every report. Other datasets remain available for regression tests
|
|
@@ -293,14 +293,24 @@ and the debugger, but have no benchmark commands or workflows.
|
|
|
293
293
|
./benchmark.sh
|
|
294
294
|
./benchmark.sh --list
|
|
295
295
|
./benchmark.sh --sample 11-left-center
|
|
296
|
+
./benchmark.sh --sample 13-rk3308-top-left-offset
|
|
296
297
|
./benchmark.sh --concurrency 8 --sample-timeout-seconds 300
|
|
297
298
|
```
|
|
298
299
|
|
|
299
300
|
Before timing the solver, the benchmark renders the selected upstream TSX/core
|
|
300
301
|
circuits and captures their exact fanout-solver constructor inputs into
|
|
301
|
-
`benchmark-results/inputs/<sample-id>.json`. Each case retains
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
`benchmark-results/inputs/<sample-id>.json`. Each case retains its complete
|
|
303
|
+
SoC fanout workload and the original clearance, differential-pair, and length-skew
|
|
304
|
+
constraints:
|
|
305
|
+
|
|
306
|
+
| SoC | Cases | Signal connections | Plane drops | Total connections | Pad obstacles |
|
|
307
|
+
| --- | ---: | ---: | ---: | ---: | ---: |
|
|
308
|
+
| AM62L | 12 | 33 | 102 | 135 | 573 |
|
|
309
|
+
| RK3308 | 12 | 49 | 113 | 162 | 451 |
|
|
310
|
+
|
|
311
|
+
Both families have nine DDR signal buses. The RK3308 samples use a 355-ball SoC
|
|
312
|
+
and 96-ball DDR3L RAM, with the RAM placed on each side at three offsets.
|
|
313
|
+
The timed workers run
|
|
304
314
|
**this checkout's solver**, not the upstream package's released solver.
|
|
305
315
|
To capture the inputs without solving, use `bun run generate:dataset31`.
|
|
306
316
|
The optional `--dataset dataset31` flag is accepted for explicit CI invocation;
|
|
@@ -335,8 +345,10 @@ are committed so route changes can be reviewed in Git. A run replaces the select
|
|
|
335
345
|
cases' snapshots and removes their stale SVGs if they no longer solve; filtered
|
|
336
346
|
runs preserve unselected snapshots. JSON reports and captured inputs remain
|
|
337
347
|
ignored. CI includes the SVGs in its benchmark artifacts.
|
|
338
|
-
Compare reports with the same budgets to track progress.
|
|
339
|
-
|
|
348
|
+
Compare reports with the same dataset revision and budgets to track progress.
|
|
349
|
+
A case is solved only when every SoC connection has validated fanout: all 135
|
|
350
|
+
connections for AM62L or all 162 for RK3308. This covers the SoC fanout phase; RAM
|
|
351
|
+
fanout and downstream inter-chip routing are separate phases. Partial, error,
|
|
340
352
|
and timeout rows are benchmark results (exit 0); invalid CLI arguments or report
|
|
341
353
|
I/O failures are command failures (nonzero exit).
|
|
342
354
|
|
|
@@ -344,7 +356,7 @@ I/O failures are command failures (nonzero exit).
|
|
|
344
356
|
|
|
345
357
|
Once `.github/workflows/benchmark.yml` is on the default branch, a repository
|
|
346
358
|
writer can comment **`/benchmark`** on an open PR. The workflow captures that
|
|
347
|
-
PR's exact head SHA, runs all
|
|
359
|
+
PR's exact head SHA, runs all 24 dataset 31 samples on a **32-vCPU Blacksmith ARM**
|
|
348
360
|
runner, then updates a status comment with solve totals, per-sample results,
|
|
349
361
|
and a link to the complete JSON/Markdown reports and captured inputs. The Actions
|
|
350
362
|
UI also supports a manual run, optionally supplying an open PR number. No custom
|
|
@@ -510,8 +522,8 @@ bun run render:dataset
|
|
|
510
522
|
bun run start
|
|
511
523
|
```
|
|
512
524
|
|
|
513
|
-
The benchmark runs
|
|
514
|
-
validation, and timing. `bun run start` opens all regression
|
|
525
|
+
The benchmark runs all 24 dataset 31 AM62L and RK3308 cases and reports solve
|
|
526
|
+
counts, validation, and timing. `bun run start` opens all regression
|
|
515
527
|
datasets in the standard tscircuit solver debugger. `bun run
|
|
516
528
|
render:dataset` writes `graphics-debug` PNGs under one subdirectory per dataset,
|
|
517
529
|
with a red shared boundary, gray component courtyards, and green fanout-exit
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { matchBusPlanLengths } from "./match-bus-lengths"
|
|
24
24
|
import {
|
|
25
25
|
getComponentDogboneViaSiteCandidates,
|
|
26
|
+
getSingleDogboneViaSiteRepairs,
|
|
26
27
|
matchComponentDogboneViaSites,
|
|
27
28
|
} from "./match-component-dogbone-via-sites"
|
|
28
29
|
import { connectionsShareElectricalNet } from "./net-identity"
|
|
@@ -467,6 +468,9 @@ function createInitialLayerAssignment(params: {
|
|
|
467
468
|
escapeLayers: string[]
|
|
468
469
|
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
469
470
|
preferOrderedCoordinatedWindingLayers: boolean
|
|
471
|
+
traceWidth: number
|
|
472
|
+
viaDiameter: number
|
|
473
|
+
clearance: number
|
|
470
474
|
}): Readonly<Record<string, string>> {
|
|
471
475
|
const {
|
|
472
476
|
buses,
|
|
@@ -513,10 +517,151 @@ function createInitialLayerAssignment(params: {
|
|
|
513
517
|
preferOrderedCoordinatedWindingLayers &&
|
|
514
518
|
busUsesCoordinatedWinding(bus)
|
|
515
519
|
) {
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
|
|
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]!
|
|
520
665
|
continue
|
|
521
666
|
}
|
|
522
667
|
const componentDirections = directionsByComponent.get(bus.componentId)!
|
|
@@ -1046,6 +1191,9 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1046
1191
|
buses: this.preparedBuses,
|
|
1047
1192
|
escapeLayers: this.config.escapeLayers,
|
|
1048
1193
|
escapeLayersByBusId: this.escapeLayersByBusId,
|
|
1194
|
+
traceWidth: this.config.traceWidth,
|
|
1195
|
+
viaDiameter: this.config.viaDiameter,
|
|
1196
|
+
clearance: this.config.clearance,
|
|
1049
1197
|
preferOrderedCoordinatedWindingLayers:
|
|
1050
1198
|
this.config.densePlaneReservationBusIds.length > 0 ||
|
|
1051
1199
|
this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
|
|
@@ -1486,6 +1634,64 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1486
1634
|
const wideBoundaryBuses = unsortedBoundaryBuses.filter(
|
|
1487
1635
|
(bus) => bus.connections.length >= 8,
|
|
1488
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
|
+
}
|
|
1489
1695
|
const hasThreeWideBoundaryBuses =
|
|
1490
1696
|
useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
|
|
1491
1697
|
const getBoundaryTargetSpan = (bus: PreparedBus) => {
|
|
@@ -2189,13 +2395,44 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2189
2395
|
bus,
|
|
2190
2396
|
]),
|
|
2191
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
|
+
}
|
|
2192
2430
|
const areAdjacentInvertedNarrowBuses = (
|
|
2193
2431
|
first: PreparedBus,
|
|
2194
2432
|
second: PreparedBus,
|
|
2195
2433
|
): boolean => {
|
|
2196
2434
|
if (
|
|
2197
2435
|
!usePadAlignedDenseRouting ||
|
|
2198
|
-
useConfiguredDensePlaneRouting ||
|
|
2199
2436
|
first.componentId !== second.componentId ||
|
|
2200
2437
|
first.exitEdge !== second.exitEdge ||
|
|
2201
2438
|
first.direction !== second.direction ||
|
|
@@ -2259,7 +2496,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2259
2496
|
// turning pair instead leaves its adjacent singleton room to escape
|
|
2260
2497
|
// before searching for an outside-package via.
|
|
2261
2498
|
const centeredPairPromotionGroups = new Set<string>()
|
|
2262
|
-
for (const singleton of
|
|
2499
|
+
for (const singleton of leadingWideSingletonBuses) {
|
|
2263
2500
|
if (getCornerBandSide(singleton.exitEdge, singleton.preferredExit))
|
|
2264
2501
|
continue
|
|
2265
2502
|
for (const pair of boundaryBuses) {
|
|
@@ -2300,6 +2537,25 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2300
2537
|
}
|
|
2301
2538
|
}
|
|
2302
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
|
+
}
|
|
2303
2559
|
let fixedViaPointsByConnectionIndex: ReadonlyMap<
|
|
2304
2560
|
number,
|
|
2305
2561
|
{ x: number; y: number }
|
|
@@ -2447,7 +2703,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2447
2703
|
useConfiguredDensePlaneRouting &&
|
|
2448
2704
|
singleLayerBus !== bus &&
|
|
2449
2705
|
!embeddedNarrowBusAlreadyRouted
|
|
2450
|
-
let
|
|
2706
|
+
let usedRepairedViaSites = false
|
|
2451
2707
|
let busPlans = (yield* routeAlternatives(
|
|
2452
2708
|
preferSingleLayerWinding
|
|
2453
2709
|
? { ...routeParams, bus: singleLayerBus }
|
|
@@ -2554,24 +2810,134 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2554
2810
|
}
|
|
2555
2811
|
}
|
|
2556
2812
|
}
|
|
2557
|
-
//
|
|
2558
|
-
//
|
|
2559
|
-
//
|
|
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.
|
|
2560
2817
|
if (
|
|
2561
2818
|
!busPlans &&
|
|
2819
|
+
useAdaptiveDensePlaneRouting &&
|
|
2562
2820
|
bus.connections.length >= 8 &&
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
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
|
+
),
|
|
2567
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
|
+
))
|
|
2568
2930
|
) {
|
|
2569
2931
|
const committedNames = new Set(
|
|
2570
2932
|
matchedPlans.map((plan) => plan.connectionName),
|
|
2571
2933
|
)
|
|
2572
|
-
const
|
|
2934
|
+
const provisionalReservationNames = new Set(
|
|
2573
2935
|
this.preparedBuses
|
|
2574
|
-
.filter(
|
|
2936
|
+
.filter(
|
|
2937
|
+
(candidate) =>
|
|
2938
|
+
candidate.termination.type === "plane" ||
|
|
2939
|
+
candidate === adjacentCenteredFieldByTurningBus.get(bus),
|
|
2940
|
+
)
|
|
2575
2941
|
.flatMap((candidate) =>
|
|
2576
2942
|
candidate.connections.map(
|
|
2577
2943
|
(connection) => connection.connection.name,
|
|
@@ -2584,10 +2950,11 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2584
2950
|
...routeParams,
|
|
2585
2951
|
fixedViaPointsByConnectionIndex: undefined,
|
|
2586
2952
|
reservedVias: routeParams.reservedVias.filter(
|
|
2587
|
-
(reserved) =>
|
|
2953
|
+
(reserved) =>
|
|
2954
|
+
!provisionalReservationNames.has(reserved.connectionName),
|
|
2588
2955
|
),
|
|
2589
2956
|
softReservedVias: routeParams.reservedVias.filter((reserved) =>
|
|
2590
|
-
|
|
2957
|
+
provisionalReservationNames.has(reserved.connectionName),
|
|
2591
2958
|
),
|
|
2592
2959
|
},
|
|
2593
2960
|
1,
|
|
@@ -2630,7 +2997,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2630
2997
|
if (rematchedPoints) {
|
|
2631
2998
|
busPlans = freePlans
|
|
2632
2999
|
fixedViaPointsByConnectionIndex = rematchedPoints
|
|
2633
|
-
|
|
3000
|
+
usedRepairedViaSites = true
|
|
2634
3001
|
}
|
|
2635
3002
|
}
|
|
2636
3003
|
}
|
|
@@ -2647,12 +3014,12 @@ export class FanoutSolver extends BaseSolver {
|
|
|
2647
3014
|
// Only pay for additional A* variants when the first topology is so
|
|
2648
3015
|
// skewed that compact meanders are unlikely to absorb the deficit.
|
|
2649
3016
|
// This keeps already-near-matched buses on the single-attempt path.
|
|
2650
|
-
// Keep
|
|
3017
|
+
// Keep repaired via sites: routeParams still carries the
|
|
2651
3018
|
// earlier provisional sites and cannot safely replace its geometry.
|
|
2652
3019
|
if (
|
|
2653
3020
|
needsRouteDiversity &&
|
|
2654
3021
|
!matchLengthsAfterPlanes &&
|
|
2655
|
-
!
|
|
3022
|
+
!usedRepairedViaSites
|
|
2656
3023
|
) {
|
|
2657
3024
|
busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
|
|
2658
3025
|
(first, second) => {
|
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
|
@@ -165,6 +165,7 @@ function getWindingTargetOrders(params: {
|
|
|
165
165
|
}): {
|
|
166
166
|
orders: PreparedConnection[][]
|
|
167
167
|
legacyOrder: PreparedConnection[]
|
|
168
|
+
ordinaryOrderCount: number
|
|
168
169
|
} {
|
|
169
170
|
const { bus, boundaryDirection, layerNames, targetLayer } = params
|
|
170
171
|
const getTargetLayer = (candidate: PreparedConnection): string =>
|
|
@@ -268,6 +269,41 @@ function getWindingTargetOrders(params: {
|
|
|
268
269
|
// but do not let sub-nanometer noise between unrelated layer bands choose
|
|
269
270
|
// the primary topology.
|
|
270
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
|
+
}
|
|
271
307
|
const seenOrders = new Set<string>()
|
|
272
308
|
const orders = candidateOrders.filter((order) => {
|
|
273
309
|
const key = order.map((candidate) => candidate.connectionIndex).join(",")
|
|
@@ -275,7 +311,7 @@ function getWindingTargetOrders(params: {
|
|
|
275
311
|
seenOrders.add(key)
|
|
276
312
|
return true
|
|
277
313
|
})
|
|
278
|
-
return { orders, legacyOrder: legacyOrderedConnections }
|
|
314
|
+
return { orders, legacyOrder: legacyOrderedConnections, ordinaryOrderCount }
|
|
279
315
|
}
|
|
280
316
|
|
|
281
317
|
function getWindingTargetRank(params: {
|
|
@@ -318,6 +354,7 @@ function getDistributedBoundaryTargetTracks(params: {
|
|
|
318
354
|
boundaryDirection: FanoutDirection
|
|
319
355
|
traceWidth: number
|
|
320
356
|
clearance: number
|
|
357
|
+
allowLayerInterleaving?: boolean
|
|
321
358
|
}): number[] | undefined {
|
|
322
359
|
const { bus, boundaryDirection, traceWidth, clearance } = params
|
|
323
360
|
if (getCornerSide(bus) || !busUsesCoordinatedWindingChannel(bus))
|
|
@@ -352,6 +389,7 @@ function getDistributedBoundaryTargetTracks(params: {
|
|
|
352
389
|
.toSorted((a, b) => a - b)
|
|
353
390
|
const pitch = traceWidth + clearance
|
|
354
391
|
if (
|
|
392
|
+
!params.allowLayerInterleaving &&
|
|
355
393
|
tracks.every(
|
|
356
394
|
(track, index) =>
|
|
357
395
|
index === 0 || track - tracks[index - 1]! >= pitch - 1e-9,
|
|
@@ -396,6 +434,7 @@ export function getBoundaryTargetTrack(params: {
|
|
|
396
434
|
layerNames?: readonly string[]
|
|
397
435
|
targetLayer?: string
|
|
398
436
|
windingOrderIndex?: number
|
|
437
|
+
allowLayerInterleaving?: boolean
|
|
399
438
|
}): number {
|
|
400
439
|
const requestedTrack = getPerpendicularAxis(
|
|
401
440
|
params.connection.exitTargetPoint ?? params.connection.targetPoint,
|
|
@@ -2708,23 +2747,30 @@ export function* routeBusAlternativesSteps(
|
|
|
2708
2747
|
windingOrderIndex?: number
|
|
2709
2748
|
preferTargetDirectedLaneBias?: boolean
|
|
2710
2749
|
localDogboneRepair?: boolean
|
|
2750
|
+
reserveTerminalExitPoints?: boolean
|
|
2751
|
+
cornerExitLaneOffset?: number
|
|
2752
|
+
allowLayerInterleaving?: boolean
|
|
2711
2753
|
}
|
|
2712
2754
|
const maximumThroughAllRouteOrderAttempts = 24
|
|
2713
|
-
const
|
|
2755
|
+
const usesDistributedWindingTargets = Boolean(
|
|
2714
2756
|
cornerSide ||
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2757
|
+
getDistributedBoundaryTargetTracks({
|
|
2758
|
+
bus,
|
|
2759
|
+
boundaryDirection,
|
|
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
|
|
2773
|
+
: 1
|
|
2728
2774
|
const uniformDogboneTerminalPatterns: CoordinatedTerminalPattern[] =
|
|
2729
2775
|
viaHandednesses.map((viaHandedness) => ({
|
|
2730
2776
|
label: `uniform-${viaHandedness}`,
|
|
@@ -2904,7 +2950,7 @@ export function* routeBusAlternativesSteps(
|
|
|
2904
2950
|
coordinatedViaPoints
|
|
2905
2951
|
? [
|
|
2906
2952
|
...Array.from(
|
|
2907
|
-
{ length:
|
|
2953
|
+
{ length: ordinaryWindingTargetOrderCount },
|
|
2908
2954
|
(_, windingOrderIndex) => ({
|
|
2909
2955
|
label: `component-matched-vias-winding-${windingOrderIndex}`,
|
|
2910
2956
|
useViaInPad: false,
|
|
@@ -2928,6 +2974,26 @@ export function* routeBusAlternativesSteps(
|
|
|
2928
2974
|
windingOrderIndex: 0,
|
|
2929
2975
|
preferTargetDirectedLaneBias: true,
|
|
2930
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
|
+
: []),
|
|
2931
2997
|
]
|
|
2932
2998
|
: []
|
|
2933
2999
|
const planeTerminationsAlreadyOccupyTheFanout = acceptedPlans.some(
|
|
@@ -2995,6 +3061,23 @@ export function* routeBusAlternativesSteps(
|
|
|
2995
3061
|
}
|
|
2996
3062
|
}
|
|
2997
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
|
+
}
|
|
2998
3081
|
const seenTerminalSignatures = new Set<string>()
|
|
2999
3082
|
for (const terminalPattern of terminalPatterns) {
|
|
3000
3083
|
const terminals = bus.connections.map((preparedConnection) => {
|
|
@@ -3004,7 +3087,8 @@ export function* routeBusAlternativesSteps(
|
|
|
3004
3087
|
? getCornerTargetTrack({
|
|
3005
3088
|
bus,
|
|
3006
3089
|
connection: preparedConnection,
|
|
3007
|
-
cornerExitLaneOffset:
|
|
3090
|
+
cornerExitLaneOffset:
|
|
3091
|
+
terminalPattern.cornerExitLaneOffset ?? cornerLaneOffsets.exit,
|
|
3008
3092
|
traceWidth,
|
|
3009
3093
|
viaDiameter,
|
|
3010
3094
|
clearance,
|
|
@@ -3014,6 +3098,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3014
3098
|
cornerBandTargetTrackOffset,
|
|
3015
3099
|
})
|
|
3016
3100
|
: getBoundaryTargetTrack({
|
|
3101
|
+
allowLayerInterleaving: terminalPattern.allowLayerInterleaving,
|
|
3017
3102
|
bus,
|
|
3018
3103
|
connection: preparedConnection,
|
|
3019
3104
|
boundaryDirection,
|
|
@@ -3101,7 +3186,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3101
3186
|
)
|
|
3102
3187
|
.join(
|
|
3103
3188
|
"|",
|
|
3104
|
-
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}`
|
|
3189
|
+
)}:${terminalPattern.maximumRouteOrderAttempts ?? "all"}:${gridStepDivisor}:${alignGridToPads}:${Boolean(terminalPattern.localDogboneRepair)}:${Boolean(terminalPattern.preferTargetDirectedLaneBias)}:${Boolean(terminalPattern.reserveTerminalExitPoints)}`
|
|
3105
3190
|
if (seenTerminalSignatures.has(terminalSignature)) continue
|
|
3106
3191
|
seenTerminalSignatures.add(terminalSignature)
|
|
3107
3192
|
const windingSteps = routeViaMinimalWindingAlternativesSteps(
|
|
@@ -3124,6 +3209,7 @@ export function* routeBusAlternativesSteps(
|
|
|
3124
3209
|
includeReverseTargetRotation: terminalPattern.localDogboneRepair,
|
|
3125
3210
|
reservedVias,
|
|
3126
3211
|
softReservedVias,
|
|
3212
|
+
reserveTerminalExitPoints: terminalPattern.reserveTerminalExitPoints,
|
|
3127
3213
|
gridStepDivisor,
|
|
3128
3214
|
preferTargetDirectedLaneBias:
|
|
3129
3215
|
terminalPattern.preferTargetDirectedLaneBias,
|
|
@@ -76,6 +76,8 @@ export interface RouteViaMinimalWindingParams {
|
|
|
76
76
|
alignGridToPads?: boolean
|
|
77
77
|
/** Defer the outermost reversed target while routing the inner terminals. */
|
|
78
78
|
includeReverseTargetRotation?: boolean
|
|
79
|
+
/** Keep earlier lanes clear of every remaining terminal exit. */
|
|
80
|
+
reserveTerminalExitPoints?: boolean
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export interface RouteViaMinimalWindingProgress {
|
|
@@ -646,6 +648,7 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
646
648
|
adaptiveRouteOrder = false,
|
|
647
649
|
alignGridToPads = false,
|
|
648
650
|
includeReverseTargetRotation = false,
|
|
651
|
+
reserveTerminalExitPoints = false,
|
|
649
652
|
} = params
|
|
650
653
|
if (
|
|
651
654
|
maximumRouteOrderAttempts !== undefined &&
|
|
@@ -906,6 +909,14 @@ export function* routeViaMinimalWindingAlternativesSteps(
|
|
|
906
909
|
return false
|
|
907
910
|
}
|
|
908
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
|
+
}
|
|
909
920
|
const segmentMinX = Math.min(segment.start.x, segment.end.x)
|
|
910
921
|
const segmentMaxX = Math.max(segment.start.x, segment.end.x)
|
|
911
922
|
const segmentMinY = Math.min(segment.start.y, segment.end.y)
|