@tscircuit/fanout-solver 0.0.49 → 0.0.50

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.
@@ -1,6 +1,6 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
- import { mergeGraphics, type GraphicsObject } from "graphics-debug"
3
+ import { type GraphicsObject, mergeGraphics } from "graphics-debug"
4
4
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
5
5
  import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
6
6
  import { buildOutputSimpleRouteJson } from "./build-output"
@@ -25,10 +25,11 @@ import {
25
25
  } from "./prepare-buses"
26
26
  import {
27
27
  fanoutPlansAreClear,
28
+ fanoutPlansAreMutuallyClear,
28
29
  type RouteBusStaticClearanceCache,
29
30
  routeBus,
30
- routeBusAlternativesSteps,
31
31
  routeBusAlternatives,
32
+ routeBusAlternativesSteps,
32
33
  } from "./route-bus"
33
34
  import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
34
35
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
@@ -55,6 +56,8 @@ interface ResolvedFanoutConfig {
55
56
  compactBusTracks: boolean
56
57
  allowBlindAndBuriedVias: boolean
57
58
  allowSameNetMerges: boolean
59
+ densePlaneReservationBusIds: readonly string[]
60
+ denseUnrestrictedPlaneRoutingBusIds: readonly string[]
58
61
  singleLayerPushAndShove: boolean
59
62
  singleLayerAdaptiveExits: boolean
60
63
  borderDistribution: FanoutBorderDistribution
@@ -85,7 +88,7 @@ interface FanoutSubsolverRequest {
85
88
  solver: BaseSolver
86
89
  }
87
90
 
88
- type FanoutWorkYield = void | FanoutSubsolverRequest
91
+ type FanoutWorkYield = undefined | FanoutSubsolverRequest
89
92
 
90
93
  class FanoutWorkSolver<T> extends BaseSolver {
91
94
  private output: T | undefined
@@ -240,6 +243,9 @@ function resolveConfig(
240
243
  compactBusTracks: options.compactBusTracks ?? false,
241
244
  allowBlindAndBuriedVias: options.allowBlindAndBuriedVias ?? true,
242
245
  allowSameNetMerges: options.allowSameNetMerges ?? false,
246
+ densePlaneReservationBusIds: options.densePlaneReservationBusIds ?? [],
247
+ denseUnrestrictedPlaneRoutingBusIds:
248
+ options.denseUnrestrictedPlaneRoutingBusIds ?? [],
243
249
  singleLayerPushAndShove: options.singleLayerPushAndShove ?? false,
244
250
  singleLayerAdaptiveExits: options.singleLayerAdaptiveExits ?? false,
245
251
  borderDistribution,
@@ -446,8 +452,14 @@ function createInitialLayerAssignment(params: {
446
452
  buses: PreparedBus[]
447
453
  escapeLayers: string[]
448
454
  escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
455
+ preferOrderedCoordinatedWindingLayers: boolean
449
456
  }): Readonly<Record<string, string>> {
450
- const { buses, escapeLayers, escapeLayersByBusId } = params
457
+ const {
458
+ buses,
459
+ escapeLayers,
460
+ escapeLayersByBusId,
461
+ preferOrderedCoordinatedWindingLayers,
462
+ } = params
451
463
  const assignment: Record<string, string> = {}
452
464
  const directionsByComponent = new Map<string, Set<PreparedBus["direction"]>>()
453
465
  let nextViaLayerIndex = 0
@@ -483,6 +495,16 @@ function createInitialLayerAssignment(params: {
483
495
  ) {
484
496
  assignment[bus.busId] = sourceLayer
485
497
  } else if (viaLayers.length > 0) {
498
+ if (
499
+ preferOrderedCoordinatedWindingLayers &&
500
+ busUsesCoordinatedWinding(bus)
501
+ ) {
502
+ // Coordinated winding treats allowedLayers as an ordered preference.
503
+ // A global round-robin index can otherwise skip a bus's first choice
504
+ // just because a previous bus had a different set of legal layers.
505
+ assignment[bus.busId] = viaLayers[0]!
506
+ continue
507
+ }
486
508
  const componentDirections = directionsByComponent.get(bus.componentId)!
487
509
  const hasOpposingDirection =
488
510
  (componentDirections.has("left") && componentDirections.has("right")) ||
@@ -1010,6 +1032,9 @@ export class FanoutSolver extends BaseSolver {
1010
1032
  buses: this.preparedBuses,
1011
1033
  escapeLayers: this.config.escapeLayers,
1012
1034
  escapeLayersByBusId: this.escapeLayersByBusId,
1035
+ preferOrderedCoordinatedWindingLayers:
1036
+ this.config.densePlaneReservationBusIds.length > 0 ||
1037
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0,
1013
1038
  }),
1014
1039
  generatedAssignments,
1015
1040
  maxAssignments: this.config.maxLayerCombinations,
@@ -1357,10 +1382,38 @@ export class FanoutSolver extends BaseSolver {
1357
1382
  busesInRoutingOrder: readonly PreparedBus[]
1358
1383
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1359
1384
  if (this.config.allowBlindAndBuriedVias) return null
1385
+ const debugDense = (...values: unknown[]) => {
1386
+ if (process.env.FANOUT_DEBUG_DENSE === "1") {
1387
+ if (
1388
+ process.env.FANOUT_DEBUG_DENSE_SUMMARY === "1" &&
1389
+ ![
1390
+ "start",
1391
+ "plane-match:preflight-failed",
1392
+ "plane-match:incremental-complete",
1393
+ "plane-match:incremental-failed",
1394
+ "plane-route:alternate-candidate-counts",
1395
+ "plane-route:alternate-search",
1396
+ "plane-route:promote-failed",
1397
+ "plane-route:alternate-choice",
1398
+ "plane-route:promote-zero-candidates",
1399
+ "length-match:complete",
1400
+ "length-match:start",
1401
+ "plane-route:failed",
1402
+ "dense-validation",
1403
+ ].includes(String(values[0]))
1404
+ ) {
1405
+ return
1406
+ }
1407
+ console.error("dense:", ...values)
1408
+ }
1409
+ }
1360
1410
 
1361
1411
  const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
1362
1412
  (bus) => bus.termination.type === "boundary",
1363
1413
  )
1414
+ const useConfiguredDensePlaneRouting =
1415
+ this.config.densePlaneReservationBusIds.length > 0 ||
1416
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
1364
1417
  const useJointBoundaryViaReservation = shouldUseJointBoundaryViaReservation(
1365
1418
  unsortedBoundaryBuses.map((bus) => bus.connections.length),
1366
1419
  )
@@ -1382,84 +1435,181 @@ export class FanoutSolver extends BaseSolver {
1382
1435
  ]),
1383
1436
  )
1384
1437
  : null
1385
- const boundaryBuses = unsortedBoundaryBuses.toSorted((first, second) => {
1386
- // Reserve the dense escape field for the widest buses first. Small
1387
- // control groups can usually route around their copper, while routing
1388
- // a two-line corner bus first can consume a critical channel needed by
1389
- // an eight-line winding bus and force the expensive fallback search.
1390
- if (useJointBoundaryViaReservation) {
1391
- const connectionCountDifference =
1392
- second.connections.length - first.connections.length
1393
- if (connectionCountDifference !== 0) return connectionCountDifference
1394
- }
1395
- const firstLayer = params.busLayerAssignments[first.busId]
1396
- const secondLayer = params.busLayerAssignments[second.busId]
1397
- const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1398
- first.busId,
1399
- )
1400
- const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1401
- second.busId,
1402
- )
1403
- if (
1404
- firstPairRoutingPriority !== undefined &&
1405
- secondPairRoutingPriority !== undefined &&
1406
- firstPairRoutingPriority !== secondPairRoutingPriority
1407
- ) {
1408
- // The third pair can be fenced off by two earlier pair windings. Let
1409
- // the pair with the shortest farthest-lane boundary reach claim its
1410
- // channel first without relying on caller-specific bus identifiers.
1411
- return firstPairRoutingPriority - secondPairRoutingPriority
1438
+ const wideBoundaryBuses = unsortedBoundaryBuses.filter(
1439
+ (bus) => bus.connections.length >= 8,
1440
+ )
1441
+ const hasThreeWideBoundaryBuses =
1442
+ useConfiguredDensePlaneRouting && wideBoundaryBuses.length === 3
1443
+ const getBoundaryTargetSpan = (bus: PreparedBus) => {
1444
+ const coordinates = bus.connections.map((connection) => {
1445
+ const target = connection.exitTargetPoint ?? connection.targetPoint
1446
+ return bus.exitEdge === "top" || bus.exitEdge === "bottom"
1447
+ ? target.x
1448
+ : target.y
1449
+ })
1450
+ return {
1451
+ minimum: Math.min(...coordinates),
1452
+ maximum: Math.max(...coordinates),
1412
1453
  }
1413
- const cornerBandDifference =
1414
- Number(
1415
- Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
1416
- ) -
1417
- Number(Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)))
1418
- if (cornerBandDifference !== 0) return cornerBandDifference
1419
- const firstIsCorner = Boolean(
1420
- getCornerBandSide(first.exitEdge, first.preferredExit),
1421
- )
1422
- if (!firstIsCorner) {
1423
- const getSourceSpan = (bus: PreparedBus): number => {
1424
- const xCoordinates = bus.connections.map(
1425
- (connection) => connection.sourcePoint.x,
1426
- )
1427
- const yCoordinates = bus.connections.map(
1428
- (connection) => connection.sourcePoint.y,
1429
- )
1430
- return (
1431
- Math.max(...xCoordinates) -
1432
- Math.min(...xCoordinates) +
1433
- Math.max(...yCoordinates) -
1434
- Math.min(...yCoordinates)
1454
+ }
1455
+ const narrowBusOverlapsWideTargetSpan = (bus: PreparedBus): boolean => {
1456
+ if (bus.connections.length >= 8) return false
1457
+ const span = getBoundaryTargetSpan(bus)
1458
+ return wideBoundaryBuses.some((wideBus) => {
1459
+ if (wideBus.exitEdge !== bus.exitEdge) return false
1460
+ const wideSpan = getBoundaryTargetSpan(wideBus)
1461
+ return (
1462
+ span.maximum >= wideSpan.minimum - 1e-9 &&
1463
+ span.minimum <= wideSpan.maximum + 1e-9
1464
+ )
1465
+ })
1466
+ }
1467
+ const getContainingWideSourceField = (
1468
+ bus: PreparedBus,
1469
+ ): PreparedBus | undefined => {
1470
+ if (bus.connections.length >= 8) return undefined
1471
+ return wideBoundaryBuses.find((wideBus) => {
1472
+ const wideXCoordinates = wideBus.connections.map(
1473
+ (connection) => connection.sourcePoint.x,
1474
+ )
1475
+ const wideYCoordinates = wideBus.connections.map(
1476
+ (connection) => connection.sourcePoint.y,
1477
+ )
1478
+ const minimumX = Math.min(...wideXCoordinates)
1479
+ const maximumX = Math.max(...wideXCoordinates)
1480
+ const minimumY = Math.min(...wideYCoordinates)
1481
+ const maximumY = Math.max(...wideYCoordinates)
1482
+ return bus.connections.every(
1483
+ (connection) =>
1484
+ connection.sourcePoint.x >= minimumX - 1e-9 &&
1485
+ connection.sourcePoint.x <= maximumX + 1e-9 &&
1486
+ connection.sourcePoint.y >= minimumY - 1e-9 &&
1487
+ connection.sourcePoint.y <= maximumY + 1e-9,
1488
+ )
1489
+ })
1490
+ }
1491
+ const narrowBusIsEmbeddedInWideSourceField = (bus: PreparedBus): boolean =>
1492
+ Boolean(getContainingWideSourceField(bus))
1493
+ const getThreeWideRoutingPriority = (bus: PreparedBus): number =>
1494
+ narrowBusOverlapsWideTargetSpan(bus) &&
1495
+ !narrowBusIsEmbeddedInWideSourceField(bus)
1496
+ ? 0
1497
+ : bus.connections.length >= 8
1498
+ ? 1
1499
+ : bus.connections.length > 1
1500
+ ? 2
1501
+ : 3
1502
+ const initiallySortedBoundaryBuses = unsortedBoundaryBuses.toSorted(
1503
+ (first, second) => {
1504
+ // Reserve the dense escape field for the widest buses first. Small
1505
+ // control groups can usually route around their copper, while routing
1506
+ // a two-line corner bus first can consume a critical channel needed by
1507
+ // an eight-line winding bus and force the expensive fallback search.
1508
+ if (useJointBoundaryViaReservation || wideBoundaryBuses.length > 0) {
1509
+ if (hasThreeWideBoundaryBuses) {
1510
+ const threeWidePriorityDifference =
1511
+ getThreeWideRoutingPriority(first) -
1512
+ getThreeWideRoutingPriority(second)
1513
+ if (threeWidePriorityDifference !== 0) {
1514
+ return threeWidePriorityDifference
1515
+ }
1516
+ }
1517
+ const connectionCountDifference =
1518
+ unsortedBoundaryBuses.length === 2 &&
1519
+ Math.max(
1520
+ ...unsortedBoundaryBuses.map((bus) => bus.connections.length),
1521
+ ) >= 8
1522
+ ? first.connections.length - second.connections.length
1523
+ : second.connections.length - first.connections.length
1524
+ if (connectionCountDifference !== 0) return connectionCountDifference
1525
+ }
1526
+ const firstLayer = params.busLayerAssignments[first.busId]
1527
+ const secondLayer = params.busLayerAssignments[second.busId]
1528
+ const firstPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1529
+ first.busId,
1530
+ )
1531
+ const secondPairRoutingPriority = pairRoutingPriorityKeyByBusId?.get(
1532
+ second.busId,
1533
+ )
1534
+ if (
1535
+ firstPairRoutingPriority !== undefined &&
1536
+ secondPairRoutingPriority !== undefined &&
1537
+ firstPairRoutingPriority !== secondPairRoutingPriority
1538
+ ) {
1539
+ // The third pair can be fenced off by two earlier pair windings. Let
1540
+ // the pair with the shortest farthest-lane boundary reach claim its
1541
+ // channel first without relying on caller-specific bus identifiers.
1542
+ return firstPairRoutingPriority - secondPairRoutingPriority
1543
+ }
1544
+ const cornerBandDifference =
1545
+ Number(
1546
+ Boolean(getCornerBandSide(second.exitEdge, second.preferredExit)),
1547
+ ) -
1548
+ Number(
1549
+ Boolean(getCornerBandSide(first.exitEdge, first.preferredExit)),
1435
1550
  )
1551
+ if (cornerBandDifference !== 0) return cornerBandDifference
1552
+ const firstIsCorner = Boolean(
1553
+ getCornerBandSide(first.exitEdge, first.preferredExit),
1554
+ )
1555
+ if (!firstIsCorner) {
1556
+ const getSourceSpan = (bus: PreparedBus): number => {
1557
+ const xCoordinates = bus.connections.map(
1558
+ (connection) => connection.sourcePoint.x,
1559
+ )
1560
+ const yCoordinates = bus.connections.map(
1561
+ (connection) => connection.sourcePoint.y,
1562
+ )
1563
+ return (
1564
+ Math.max(...xCoordinates) -
1565
+ Math.min(...xCoordinates) +
1566
+ Math.max(...yCoordinates) -
1567
+ Math.min(...yCoordinates)
1568
+ )
1569
+ }
1570
+ const sourceSpanDifference =
1571
+ getSourceSpan(second) - getSourceSpan(first)
1572
+ if (Math.abs(sourceSpanDifference) > 1e-9) {
1573
+ return sourceSpanDifference
1574
+ }
1436
1575
  }
1437
- const sourceSpanDifference =
1438
- getSourceSpan(second) - getSourceSpan(first)
1439
- if (Math.abs(sourceSpanDifference) > 1e-9) {
1440
- return sourceSpanDifference
1576
+ const layerDifference =
1577
+ this.config.layerNames.indexOf(firstLayer ?? "") -
1578
+ this.config.layerNames.indexOf(secondLayer ?? "")
1579
+ if (layerDifference !== 0) return -layerDifference
1580
+ if (
1581
+ unsortedBoundaryBuses.length !== 6 &&
1582
+ unsortedBoundaryBuses.length !== 7 &&
1583
+ unsortedBoundaryBuses.length !== 8 &&
1584
+ unsortedBoundaryBuses.length !== 9
1585
+ ) {
1586
+ return 0
1441
1587
  }
1442
- }
1443
- const layerDifference =
1444
- this.config.layerNames.indexOf(firstLayer ?? "") -
1445
- this.config.layerNames.indexOf(secondLayer ?? "")
1446
- if (layerDifference !== 0) return -layerDifference
1447
- if (
1448
- unsortedBoundaryBuses.length !== 6 &&
1449
- unsortedBoundaryBuses.length !== 7 &&
1450
- unsortedBoundaryBuses.length !== 8 &&
1451
- unsortedBoundaryBuses.length !== 9
1452
- ) {
1453
- return 0
1454
- }
1455
- // The general routing order can differ across the two components as a
1456
- // function of local pad geometry. Keep otherwise-equivalent corner buses
1457
- // in one deterministic order for the six- through nine-bus paths so their
1458
- // boundary lanes do not swap between the two ends of a direct
1459
- // interconnect. Leave the released four- and five-bus tie behavior
1460
- // unchanged.
1461
- return first.busId.localeCompare(second.busId)
1462
- })
1588
+ // The general routing order can differ across the two components as a
1589
+ // function of local pad geometry. Keep otherwise-equivalent corner buses
1590
+ // in one deterministic order for the six- through nine-bus paths so their
1591
+ // boundary lanes do not swap between the two ends of a direct
1592
+ // interconnect. Leave the released four- and five-bus tie behavior
1593
+ // unchanged.
1594
+ return first.busId.localeCompare(second.busId)
1595
+ },
1596
+ )
1597
+ const debugBoundaryOrder =
1598
+ process.env.FANOUT_DEBUG_BOUNDARY_ORDER?.split(",") ??
1599
+ (process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS
1600
+ ? [process.env.FANOUT_DEBUG_FIRST_BOUNDARY_BUS]
1601
+ : [])
1602
+ const boundaryBuses =
1603
+ debugBoundaryOrder.length > 0
1604
+ ? initiallySortedBoundaryBuses.toSorted((first, second) => {
1605
+ const firstIndex = debugBoundaryOrder.indexOf(first.busId)
1606
+ const secondIndex = debugBoundaryOrder.indexOf(second.busId)
1607
+ return (
1608
+ (firstIndex < 0 ? Number.POSITIVE_INFINITY : firstIndex) -
1609
+ (secondIndex < 0 ? Number.POSITIVE_INFINITY : secondIndex)
1610
+ )
1611
+ })
1612
+ : initiallySortedBoundaryBuses
1463
1613
  // Preserve the caller/input order for the dense singleton fill. The
1464
1614
  // general routing sort is useful for heterogeneous buses, but ordering a
1465
1615
  // regular BGA power field by obstacle depth creates artificial local
@@ -1468,6 +1618,43 @@ export class FanoutSolver extends BaseSolver {
1468
1618
  const planeBuses = this.preparedBuses.filter(
1469
1619
  (bus) => bus.termination.type === "plane",
1470
1620
  )
1621
+ const denseAdditionalObstacles = useConfiguredDensePlaneRouting
1622
+ ? this.routingSrj.obstacles
1623
+ : undefined
1624
+ const initialPlaneReservationCount = Number.parseInt(
1625
+ process.env.FANOUT_INITIAL_PLANE_RESERVATIONS ?? "8",
1626
+ 10,
1627
+ )
1628
+ const debugInitialPlaneIndices =
1629
+ process.env.FANOUT_DEBUG_INITIAL_PLANE_INDICES?.split(",").map(
1630
+ (index) => Number(index) - 1,
1631
+ )
1632
+ const debugInitialPlaneBusIds =
1633
+ process.env.FANOUT_DEBUG_INITIAL_PLANE_BUS_IDS?.split(",")
1634
+ let activeBoundaryReservationPlaneBuses = debugInitialPlaneBusIds
1635
+ ? planeBuses.filter((bus) => debugInitialPlaneBusIds.includes(bus.busId))
1636
+ : debugInitialPlaneIndices
1637
+ ? debugInitialPlaneIndices.flatMap((index) =>
1638
+ planeBuses[index] ? [planeBuses[index]!] : [],
1639
+ )
1640
+ : this.config.densePlaneReservationBusIds.length > 0
1641
+ ? planeBuses.filter((bus) =>
1642
+ this.config.densePlaneReservationBusIds.includes(bus.busId),
1643
+ )
1644
+ : useConfiguredDensePlaneRouting
1645
+ ? planeBuses.slice(
1646
+ 0,
1647
+ Number.isFinite(initialPlaneReservationCount)
1648
+ ? initialPlaneReservationCount
1649
+ : 8,
1650
+ )
1651
+ : planeBuses
1652
+ debugDense(
1653
+ "start",
1654
+ boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
1655
+ `planes:${planeBuses.length}`,
1656
+ `joint:${useJointBoundaryViaReservation}`,
1657
+ )
1471
1658
  if (
1472
1659
  boundaryBuses.length === 0 ||
1473
1660
  boundaryBuses.length > 9 ||
@@ -1498,18 +1685,40 @@ export class FanoutSolver extends BaseSolver {
1498
1685
  singletonBoundaryBusCount > 1 &&
1499
1686
  shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
1500
1687
  const preferredBoundaryPerpendicularSideByBusId = new Map(
1501
- boundaryBuses.map((bus) => [bus.busId, 1 as const]),
1688
+ boundaryBuses.map((bus) => [
1689
+ bus.busId,
1690
+ hasThreeWideBoundaryBuses &&
1691
+ bus.connections.length < 8 &&
1692
+ narrowBusIsEmbeddedInWideSourceField(bus)
1693
+ ? (-1 as const)
1694
+ : (1 as const),
1695
+ ]),
1502
1696
  )
1503
1697
  const preferBoundaryOutwardByBusId = new Map(
1504
1698
  boundaryBuses.map((bus) => [
1505
1699
  bus.busId,
1506
- useGeometryAwareSingletonOutwardPreference &&
1507
- bus.connections.length === 1 &&
1508
- getCornerBandSide(bus.exitEdge, bus.preferredExit)
1509
- ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1510
- : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1700
+ bus.connections.length === 1
1701
+ ? hasThreeWideBoundaryBuses &&
1702
+ narrowBusIsEmbeddedInWideSourceField(bus)
1703
+ ? true
1704
+ : useGeometryAwareSingletonOutwardPreference &&
1705
+ getCornerBandSide(bus.exitEdge, bus.preferredExit)
1706
+ ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1707
+ : getExitEdgeForDirection(bus.direction) !== bus.exitEdge
1708
+ : bus.exitEdge === "top" && bus.connections.length >= 8
1709
+ ? false
1710
+ : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1511
1711
  ]),
1512
1712
  )
1713
+ const debugFlippedBoundaryBus = process.env.FANOUT_DEBUG_FLIP_BOUNDARY_BUS
1714
+ if (debugFlippedBoundaryBus) {
1715
+ preferredBoundaryPerpendicularSideByBusId.set(debugFlippedBoundaryBus, -1)
1716
+ }
1717
+ const debugOutwardBoundaryBus =
1718
+ process.env.FANOUT_DEBUG_OUTWARD_BOUNDARY_BUS
1719
+ if (debugOutwardBoundaryBus) {
1720
+ preferBoundaryOutwardByBusId.set(debugOutwardBoundaryBus, true)
1721
+ }
1513
1722
  const canShareCopper = (
1514
1723
  firstConnectionIndex: number,
1515
1724
  secondConnectionIndex: number,
@@ -1545,6 +1754,7 @@ export class FanoutSolver extends BaseSolver {
1545
1754
  (bus) => bus.connections.length === 1,
1546
1755
  )
1547
1756
  const singletonDeferralCandidates =
1757
+ !hasThreeWideBoundaryBuses &&
1548
1758
  shouldDeferSingletonBoundaryViaReservation(boundaryBusConnectionCounts)
1549
1759
  ? singletonBoundaryBuses
1550
1760
  .toSorted(
@@ -1559,8 +1769,8 @@ export class FanoutSolver extends BaseSolver {
1559
1769
  ),
1560
1770
  )
1561
1771
  : []
1562
- const leadingWideSingletonBuses =
1563
- boundaryBuses.length === 9
1772
+ const multiLayerLeadingSingletonBuses =
1773
+ boundaryBuses.length === 8 || boundaryBuses.length === 9
1564
1774
  ? singletonDeferralCandidates.filter((singletonBus) => {
1565
1775
  const singletonTargetLayer =
1566
1776
  params.busLayerAssignments[singletonBus.busId]
@@ -1574,6 +1784,53 @@ export class FanoutSolver extends BaseSolver {
1574
1784
  )
1575
1785
  })
1576
1786
  : []
1787
+ const throughAllLeadingSingletonBuses = hasThreeWideBoundaryBuses
1788
+ ? singletonBoundaryBuses.filter((singletonBus) => {
1789
+ const containingWideBus = getContainingWideSourceField(singletonBus)
1790
+ const singletonTargetLayer =
1791
+ params.busLayerAssignments[singletonBus.busId]
1792
+ const containingWideLayers =
1793
+ containingWideBus?.routableEscapeLayers ??
1794
+ containingWideBus?.allowedLayers ??
1795
+ []
1796
+ return Boolean(
1797
+ containingWideBus &&
1798
+ singletonTargetLayer &&
1799
+ !containingWideLayers.includes(singletonTargetLayer),
1800
+ )
1801
+ })
1802
+ : []
1803
+ const throughAllLeadingCompanionBuses =
1804
+ throughAllLeadingSingletonBuses.flatMap((singletonBus) => {
1805
+ const containingWideBus = getContainingWideSourceField(singletonBus)
1806
+ const singletonTargetLayer =
1807
+ params.busLayerAssignments[singletonBus.busId]
1808
+ return boundaryBuses.filter(
1809
+ (candidate) =>
1810
+ candidate.connections.length > 1 &&
1811
+ candidate.connections.length < 8 &&
1812
+ candidate.direction === singletonBus.direction &&
1813
+ params.busLayerAssignments[candidate.busId] ===
1814
+ singletonTargetLayer &&
1815
+ getContainingWideSourceField(candidate) === containingWideBus,
1816
+ )
1817
+ })
1818
+ const throughAllLeadingBuses =
1819
+ process.env.FANOUT_DEBUG_DISABLE_LEADING_NARROW === "1"
1820
+ ? []
1821
+ : throughAllLeadingSingletonBuses.flatMap((singletonBus) => [
1822
+ singletonBus,
1823
+ ...throughAllLeadingCompanionBuses.filter(
1824
+ (candidate) =>
1825
+ candidate.direction === singletonBus.direction &&
1826
+ params.busLayerAssignments[candidate.busId] ===
1827
+ params.busLayerAssignments[singletonBus.busId],
1828
+ ),
1829
+ ])
1830
+ const leadingWideSingletonBuses = [
1831
+ ...multiLayerLeadingSingletonBuses,
1832
+ ...throughAllLeadingBuses,
1833
+ ].filter((bus, index, buses) => buses.indexOf(bus) === index)
1577
1834
  const leadingLaneCountByWideCornerBand = new Map<string, number>()
1578
1835
  if (boundaryBuses.length === 9 && leadingWideSingletonBuses.length > 0) {
1579
1836
  for (const bus of leadingWideSingletonBuses) {
@@ -1598,11 +1855,22 @@ export class FanoutSolver extends BaseSolver {
1598
1855
  )
1599
1856
  }
1600
1857
  }
1601
- const viaProvisionalSingletonBusSet = new Set(
1602
- singletonDeferralCandidates.filter(
1858
+ const viaProvisionalBoundaryBusSet = new Set([
1859
+ ...(process.env.FANOUT_DEBUG_PROVISIONAL_NARROW === "1"
1860
+ ? boundaryBuses.filter((bus) => bus.connections.length < 8)
1861
+ : []),
1862
+ ...singletonDeferralCandidates.filter(
1603
1863
  (bus) => !leadingWideSingletonBuses.includes(bus),
1604
1864
  ),
1605
- )
1865
+ ...(hasThreeWideBoundaryBuses
1866
+ ? boundaryBuses.filter(
1867
+ (bus) =>
1868
+ bus.connections.length === 2 &&
1869
+ !narrowBusOverlapsWideTargetSpan(bus) &&
1870
+ !leadingWideSingletonBuses.includes(bus),
1871
+ )
1872
+ : []),
1873
+ ])
1606
1874
  const getCornerBandTargetTrackOffset = (bus: PreparedBus): number => {
1607
1875
  const side = getCornerBandSide(bus.exitEdge, bus.preferredExit)
1608
1876
  if (!bus.exitEdge || !side) return 0
@@ -1616,11 +1884,14 @@ export class FanoutSolver extends BaseSolver {
1616
1884
  })
1617
1885
  }
1618
1886
  const initiallyMatchedBoundaryBuses = boundaryBuses.filter(
1619
- (bus) => !viaProvisionalSingletonBusSet.has(bus),
1887
+ (bus) => !viaProvisionalBoundaryBusSet.has(bus),
1620
1888
  )
1621
1889
  const jointViaPoints = useJointBoundaryViaReservation
1622
1890
  ? matchComponentDogboneViaSites(
1623
- [...planeBuses, ...initiallyMatchedBoundaryBuses],
1891
+ [
1892
+ ...activeBoundaryReservationPlaneBuses,
1893
+ ...initiallyMatchedBoundaryBuses,
1894
+ ],
1624
1895
  {
1625
1896
  viaDiameter: this.config.viaDiameter,
1626
1897
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1629,22 +1900,108 @@ export class FanoutSolver extends BaseSolver {
1629
1900
  maximumSearchStates: 100_000,
1630
1901
  preferredBoundaryPerpendicularSideByBusId,
1631
1902
  preferBoundaryOutwardByBusId,
1903
+ additionalObstacles: denseAdditionalObstacles,
1904
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1632
1905
  canShareCopper,
1633
1906
  },
1634
1907
  )
1635
1908
  : null
1636
- const seedViaPoints =
1909
+ let seedViaPoints =
1637
1910
  jointViaPoints ??
1638
- matchComponentDogboneViaSites([...planeBuses, boundaryBuses[0]!], {
1639
- viaDiameter: this.config.viaDiameter,
1640
- viaHoleDiameter: this.config.viaHoleDiameter,
1641
- traceWidth: this.config.traceWidth,
1642
- clearance: this.config.clearance,
1643
- maximumSearchStates: 20_000,
1644
- preferredBoundaryPerpendicularSideByBusId,
1645
- preferBoundaryOutwardByBusId,
1646
- canShareCopper,
1647
- })
1911
+ matchComponentDogboneViaSites(
1912
+ [...activeBoundaryReservationPlaneBuses, boundaryBuses[0]!],
1913
+ {
1914
+ viaDiameter: this.config.viaDiameter,
1915
+ viaHoleDiameter: this.config.viaHoleDiameter,
1916
+ traceWidth: this.config.traceWidth,
1917
+ clearance: this.config.clearance,
1918
+ maximumSearchStates: 20_000,
1919
+ preferredBoundaryPerpendicularSideByBusId,
1920
+ preferBoundaryOutwardByBusId,
1921
+ additionalObstacles: denseAdditionalObstacles,
1922
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1923
+ canShareCopper,
1924
+ },
1925
+ )
1926
+ const debugFixedVias = process.env.FANOUT_DEBUG_FIXED_VIAS
1927
+ if (seedViaPoints && debugFixedVias) {
1928
+ seedViaPoints = new Map(seedViaPoints)
1929
+ for (const entry of debugFixedVias.split(",")) {
1930
+ const parts = entry.split(":")
1931
+ const rawY = parts.pop()
1932
+ const rawX = parts.pop()
1933
+ const connectionName = parts.join(":")
1934
+ const connectionIndex = [...connectionNameByIndex].find(
1935
+ ([, name]) => name === connectionName,
1936
+ )?.[0]
1937
+ const x = Number(rawX)
1938
+ const y = Number(rawY)
1939
+ if (
1940
+ connectionIndex !== undefined &&
1941
+ Number.isFinite(x) &&
1942
+ Number.isFinite(y)
1943
+ ) {
1944
+ seedViaPoints.set(connectionIndex, { x, y })
1945
+ }
1946
+ }
1947
+ }
1948
+ const debugStagedPlaneBuses = process.env.FANOUT_DEBUG_STAGED_PLANE_BUS_IDS
1949
+ ? planeBuses.filter((bus) =>
1950
+ process.env
1951
+ .FANOUT_DEBUG_STAGED_PLANE_BUS_IDS!.split(",")
1952
+ .includes(bus.busId),
1953
+ )
1954
+ : (process.env.FANOUT_DEBUG_STAGED_PLANE_INDICES?.split(",").flatMap(
1955
+ (index) =>
1956
+ planeBuses[Number(index) - 1]
1957
+ ? [planeBuses[Number(index) - 1]!]
1958
+ : [],
1959
+ ) ?? [])
1960
+ if (seedViaPoints && debugStagedPlaneBuses.length > 0) {
1961
+ for (const stagedPlaneBus of debugStagedPlaneBuses) {
1962
+ if (activeBoundaryReservationPlaneBuses.includes(stagedPlaneBus)) {
1963
+ continue
1964
+ }
1965
+ const stagedViaPoints = matchComponentDogboneViaSites(
1966
+ [
1967
+ ...activeBoundaryReservationPlaneBuses,
1968
+ stagedPlaneBus,
1969
+ ...initiallyMatchedBoundaryBuses,
1970
+ ],
1971
+ {
1972
+ viaDiameter: this.config.viaDiameter,
1973
+ viaHoleDiameter: this.config.viaHoleDiameter,
1974
+ traceWidth: this.config.traceWidth,
1975
+ clearance: this.config.clearance,
1976
+ maximumSearchStates: 100_000,
1977
+ preferredBoundaryPerpendicularSideByBusId,
1978
+ preferBoundaryOutwardByBusId,
1979
+ fixedViaPointsByConnectionIndex: seedViaPoints,
1980
+ additionalObstacles: denseAdditionalObstacles,
1981
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1982
+ canShareCopper,
1983
+ },
1984
+ )
1985
+ debugDense(
1986
+ "plane-reservation:staged",
1987
+ stagedPlaneBus.busId,
1988
+ stagedViaPoints?.size ?? "failed",
1989
+ )
1990
+ if (!stagedViaPoints) break
1991
+ seedViaPoints = new Map([...seedViaPoints, ...stagedViaPoints])
1992
+ activeBoundaryReservationPlaneBuses.push(stagedPlaneBus)
1993
+ }
1994
+ }
1995
+ debugDense("seed", seedViaPoints?.size ?? "failed")
1996
+ if (seedViaPoints && process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
1997
+ console.error(
1998
+ "dense: seed-points",
1999
+ [...seedViaPoints].map(([connectionIndex, point]) => ({
2000
+ connection: connectionNameByIndex.get(connectionIndex),
2001
+ point,
2002
+ })),
2003
+ )
2004
+ }
1648
2005
  let denseWorkUnitIndex = 1
1649
2006
  const denseWorkUnitCount = boundaryBuses.length + planeBuses.length + 3
1650
2007
  this.setInProgressPlans({
@@ -1657,10 +2014,19 @@ export class FanoutSolver extends BaseSolver {
1657
2014
  yield
1658
2015
  if (seedViaPoints) {
1659
2016
  const denseBoundaryBusesInRoutingOrder = [
1660
- ...leadingWideSingletonBuses,
1661
- ...boundaryBuses.filter(
1662
- (bus) => !leadingWideSingletonBuses.includes(bus),
1663
- ),
2017
+ ...multiLayerLeadingSingletonBuses,
2018
+ ...boundaryBuses
2019
+ .filter(
2020
+ (bus) =>
2021
+ !multiLayerLeadingSingletonBuses.includes(bus) &&
2022
+ !throughAllLeadingBuses.includes(bus),
2023
+ )
2024
+ .flatMap((bus) => [
2025
+ ...throughAllLeadingBuses.filter(
2026
+ (candidate) => getContainingWideSourceField(candidate) === bus,
2027
+ ),
2028
+ bus,
2029
+ ]),
1664
2030
  ]
1665
2031
  let fixedViaPointsByConnectionIndex: ReadonlyMap<
1666
2032
  number,
@@ -1704,6 +2070,20 @@ export class FanoutSolver extends BaseSolver {
1704
2070
  this: FanoutSolver,
1705
2071
  bus: PreparedBus,
1706
2072
  ): Generator<FanoutWorkYield, boolean, unknown> {
2073
+ debugDense("route:start", bus.busId, matchedPlans.length)
2074
+ if (process.env.FANOUT_DEBUG_DENSE_POINTS === "1") {
2075
+ console.error(
2076
+ "dense: points",
2077
+ bus.busId,
2078
+ bus.connections.map((connection) => ({
2079
+ source: connection.sourcePoint,
2080
+ via: fixedViaPointsByConnectionIndex.get(
2081
+ connection.connectionIndex,
2082
+ ),
2083
+ target: connection.exitTargetPoint ?? connection.targetPoint,
2084
+ })),
2085
+ )
2086
+ }
1707
2087
  const targetLayer = params.busLayerAssignments[bus.busId]
1708
2088
  if (!targetLayer) {
1709
2089
  return false
@@ -1724,18 +2104,22 @@ export class FanoutSolver extends BaseSolver {
1724
2104
  staticClearanceCache: this.routeStaticClearanceCache,
1725
2105
  fixedViaPointsByConnectionIndex,
1726
2106
  reservedVias: getReservedVias(bus),
1727
- viaMinimalOnly: true,
2107
+ viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2108
+ fixedViaFallbackRouteOrderAttempts: useConfiguredDensePlaneRouting
2109
+ ? 6
2110
+ : 24,
1728
2111
  cornerBandTargetTrackOffset: getCornerBandTargetTrackOffset(bus),
1729
2112
  } as const
1730
2113
  const routeAlternatives = function* (
1731
2114
  this: FanoutSolver,
2115
+ candidateRouteParams: Parameters<typeof routeBusAlternativesSteps>[0],
1732
2116
  maximumAlternatives: number,
1733
2117
  ): Generator<FanoutWorkYield, FanoutRoutePlan[][], unknown> {
1734
2118
  this.activeRoutingVisualization = null
1735
2119
  const solver = this.createWorkSolver(
1736
2120
  "BoundaryBusRoutingSolver",
1737
2121
  this.routeBusAlternativesWorkSteps(
1738
- routeParams,
2122
+ candidateRouteParams,
1739
2123
  maximumAlternatives,
1740
2124
  ),
1741
2125
  undefined,
@@ -1743,7 +2127,37 @@ export class FanoutSolver extends BaseSolver {
1743
2127
  )
1744
2128
  return (yield { type: "subsolver", solver }) as FanoutRoutePlan[][]
1745
2129
  }.bind(this)
1746
- let busPlans = (yield* routeAlternatives(1))[0]
2130
+ const routableEscapeLayers =
2131
+ bus.routableEscapeLayers ?? bus.allowedLayers ?? []
2132
+ const singleLayerBus = routableEscapeLayers.some(
2133
+ (layer) => layer !== targetLayer,
2134
+ )
2135
+ ? { ...bus, routableEscapeLayers: [targetLayer] }
2136
+ : bus
2137
+ const embeddedNarrowBusAlreadyRouted = boundaryBuses.some(
2138
+ (candidate) =>
2139
+ candidate.connections.length < 8 &&
2140
+ getContainingWideSourceField(candidate) === bus &&
2141
+ matchedPlans.some((plan) => plan.busId === candidate.busId),
2142
+ )
2143
+ // In the configured dense-plane mode, a second allowed layer is an
2144
+ // optional winding crossover channel rather than a requirement. Try
2145
+ // the simpler single-layer route first unless an already-routed narrow
2146
+ // bus occupies that direct winding channel. Preserve the released
2147
+ // multi-layer search order for callers that did not opt into this mode.
2148
+ const preferSingleLayerWinding =
2149
+ useConfiguredDensePlaneRouting &&
2150
+ singleLayerBus !== bus &&
2151
+ !embeddedNarrowBusAlreadyRouted
2152
+ let busPlans = (yield* routeAlternatives(
2153
+ preferSingleLayerWinding
2154
+ ? { ...routeParams, bus: singleLayerBus }
2155
+ : routeParams,
2156
+ 1,
2157
+ ))[0]
2158
+ if (!busPlans && preferSingleLayerWinding) {
2159
+ busPlans = (yield* routeAlternatives(routeParams, 1))[0]
2160
+ }
1747
2161
  if (busPlans && bus.maxLengthSkew !== undefined) {
1748
2162
  const lengths = busPlans.map((plan) => plan.length)
1749
2163
  const rawSkew = Math.max(...lengths) - Math.min(...lengths)
@@ -1758,7 +2172,7 @@ export class FanoutSolver extends BaseSolver {
1758
2172
  // skewed that compact meanders are unlikely to absorb the deficit.
1759
2173
  // This keeps already-near-matched buses on the single-attempt path.
1760
2174
  if (needsRouteDiversity) {
1761
- busPlans = (yield* routeAlternatives(3)).toSorted(
2175
+ busPlans = (yield* routeAlternatives(routeParams, 3)).toSorted(
1762
2176
  (first, second) => {
1763
2177
  const firstLengths = first.map((plan) => plan.length)
1764
2178
  const secondLengths = second.map((plan) => plan.length)
@@ -1772,14 +2186,29 @@ export class FanoutSolver extends BaseSolver {
1772
2186
  }
1773
2187
  }
1774
2188
  if (!busPlans) {
2189
+ debugDense("route:failed", bus.busId)
1775
2190
  return false
1776
2191
  }
1777
2192
  matchedPlans.push(...busPlans)
2193
+ debugDense("route:complete", bus.busId, busPlans.length)
1778
2194
  return true
1779
2195
  }.bind(this)
1780
2196
 
1781
2197
  const firstBoundaryBus = denseBoundaryBusesInRoutingOrder[0]!
1782
2198
  const routedBoundaryBuses: PreparedBus[] = []
2199
+ const reserveAllPlaneDogbonesAfterFirstWideBus = (
2200
+ bus: PreparedBus,
2201
+ ): void => {
2202
+ if (
2203
+ bus.connections.length >= 8 &&
2204
+ !useConfiguredDensePlaneRouting &&
2205
+ process.env.FANOUT_DEBUG_NO_PLANE_EXPANSION !== "1" &&
2206
+ activeBoundaryReservationPlaneBuses.length < planeBuses.length
2207
+ ) {
2208
+ activeBoundaryReservationPlaneBuses = planeBuses
2209
+ debugDense("plane-reservations:expanded", planeBuses.length)
2210
+ }
2211
+ }
1783
2212
  const firstBoundaryBusRouted =
1784
2213
  yield* routeMatchedBoundaryBusSteps(firstBoundaryBus)
1785
2214
  this.setInProgressPlans({
@@ -1793,6 +2222,7 @@ export class FanoutSolver extends BaseSolver {
1793
2222
  yield
1794
2223
  if (firstBoundaryBusRouted) {
1795
2224
  routedBoundaryBuses.push(firstBoundaryBus)
2225
+ reserveAllPlaneDogbonesAfterFirstWideBus(firstBoundaryBus)
1796
2226
  } else {
1797
2227
  matchedRoutingSucceeded = false
1798
2228
  }
@@ -1811,18 +2241,51 @@ export class FanoutSolver extends BaseSolver {
1811
2241
  candidateIndex++
1812
2242
  ) {
1813
2243
  const candidateBus = remainingBoundaryBuses[candidateIndex]!
2244
+ debugDense("candidate:start", candidateBus.busId)
2245
+ const candidateMatchingBase = new Map(fixedViaPointsByConnectionIndex)
2246
+ const debugLateFixedVias = process.env.FANOUT_DEBUG_LATE_FIXED_VIAS
2247
+ if (debugLateFixedVias) {
2248
+ const candidateConnectionIndices = new Set(
2249
+ candidateBus.connections.map(
2250
+ (connection) => connection.connectionIndex,
2251
+ ),
2252
+ )
2253
+ for (const entry of debugLateFixedVias.split(",")) {
2254
+ const parts = entry.split(":")
2255
+ const rawY = parts.pop()
2256
+ const rawX = parts.pop()
2257
+ const connectionName = parts.join(":")
2258
+ const connectionIndex = [...connectionNameByIndex].find(
2259
+ ([, name]) => name === connectionName,
2260
+ )?.[0]
2261
+ const x = Number(rawX)
2262
+ const y = Number(rawY)
2263
+ if (
2264
+ connectionIndex !== undefined &&
2265
+ candidateConnectionIndices.has(connectionIndex) &&
2266
+ Number.isFinite(x) &&
2267
+ Number.isFinite(y)
2268
+ ) {
2269
+ candidateMatchingBase.set(connectionIndex, { x, y })
2270
+ }
2271
+ }
2272
+ }
1814
2273
  const candidateHasFixedViaPoints = candidateBus.connections.every(
1815
2274
  (connection) =>
1816
- fixedViaPointsByConnectionIndex.has(connection.connectionIndex),
2275
+ candidateMatchingBase.has(connection.connectionIndex),
1817
2276
  )
1818
- const extendedViaPoints =
2277
+ const newlyMatchedViaPoints =
1819
2278
  jointViaPoints && candidateHasFixedViaPoints
1820
2279
  ? // The joint map is deliberately kept intact so getReservedVias()
1821
2280
  // blocks every already-reserved future through-barrel during A*.
1822
2281
  // Provisional singleton and plane dogbones are rematched later.
1823
- new Map(fixedViaPointsByConnectionIndex)
2282
+ new Map(candidateMatchingBase)
1824
2283
  : matchComponentDogboneViaSites(
1825
- [...planeBuses, ...routedBoundaryBuses, candidateBus],
2284
+ [
2285
+ ...activeBoundaryReservationPlaneBuses,
2286
+ ...routedBoundaryBuses,
2287
+ candidateBus,
2288
+ ],
1826
2289
  {
1827
2290
  viaDiameter: this.config.viaDiameter,
1828
2291
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1831,24 +2294,22 @@ export class FanoutSolver extends BaseSolver {
1831
2294
  maximumSearchStates: 100_000,
1832
2295
  preferredBoundaryPerpendicularSideByBusId,
1833
2296
  preferBoundaryOutwardByBusId,
1834
- fixedViaPointsByConnectionIndex:
1835
- fixedViaPointsByConnectionIndex,
2297
+ fixedViaPointsByConnectionIndex: candidateMatchingBase,
1836
2298
  blockingSegments,
2299
+ additionalObstacles: denseAdditionalObstacles,
2300
+ preferPlaneCheckerboardSites:
2301
+ useConfiguredDensePlaneRouting,
1837
2302
  canShareCopper,
1838
2303
  },
1839
2304
  )
1840
- if (!extendedViaPoints) {
1841
- this.setInProgressPlans({
1842
- phase: "reserve-next-dense-boundary-bus",
1843
- plans: matchedPlans,
1844
- strategy: "default",
1845
- unitIndex: denseWorkUnitIndex,
1846
- unitCount: denseWorkUnitCount,
1847
- busId: candidateBus.busId,
1848
- })
1849
- yield
1850
- continue
1851
- }
2305
+ const extendedViaPoints = newlyMatchedViaPoints
2306
+ ? new Map([...candidateMatchingBase, ...newlyMatchedViaPoints])
2307
+ : null
2308
+ debugDense(
2309
+ "candidate:matched",
2310
+ candidateBus.busId,
2311
+ extendedViaPoints?.size ?? "failed",
2312
+ )
1852
2313
  this.setInProgressPlans({
1853
2314
  phase: "reserve-next-dense-boundary-bus",
1854
2315
  plans: matchedPlans,
@@ -1858,6 +2319,7 @@ export class FanoutSolver extends BaseSolver {
1858
2319
  busId: candidateBus.busId,
1859
2320
  })
1860
2321
  yield
2322
+ if (!extendedViaPoints) continue
1861
2323
  const previousFixedViaPoints = fixedViaPointsByConnectionIndex
1862
2324
  const previousPlanCount = matchedPlans.length
1863
2325
  const laterBuses = remainingBoundaryBuses.filter(
@@ -1870,7 +2332,12 @@ export class FanoutSolver extends BaseSolver {
1870
2332
  if (laterBuses.length === 1) {
1871
2333
  const laterBus = laterBuses[0]!
1872
2334
  const futureAssignment = matchComponentDogboneViaSites(
1873
- [...planeBuses, ...routedBoundaryBuses, candidateBus, laterBus],
2335
+ [
2336
+ ...activeBoundaryReservationPlaneBuses,
2337
+ ...routedBoundaryBuses,
2338
+ candidateBus,
2339
+ laterBus,
2340
+ ],
1874
2341
  {
1875
2342
  viaDiameter: this.config.viaDiameter,
1876
2343
  viaHoleDiameter: this.config.viaHoleDiameter,
@@ -1881,9 +2348,16 @@ export class FanoutSolver extends BaseSolver {
1881
2348
  preferBoundaryOutwardByBusId,
1882
2349
  fixedViaPointsByConnectionIndex: extendedViaPoints,
1883
2350
  blockingSegments,
2351
+ additionalObstacles: denseAdditionalObstacles,
2352
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1884
2353
  canShareCopper,
1885
2354
  },
1886
2355
  )
2356
+ debugDense(
2357
+ "future:matched",
2358
+ laterBus.busId,
2359
+ futureAssignment?.size ?? "failed",
2360
+ )
1887
2361
  if (futureAssignment) {
1888
2362
  const candidateCountByConnectionIndex = new Map<number, number>()
1889
2363
  for (const candidate of getComponentDogboneViaSiteCandidates(
@@ -1894,6 +2368,8 @@ export class FanoutSolver extends BaseSolver {
1894
2368
  traceWidth: this.config.traceWidth,
1895
2369
  clearance: this.config.clearance,
1896
2370
  blockingSegments,
2371
+ additionalObstacles: denseAdditionalObstacles,
2372
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
1897
2373
  canShareCopper,
1898
2374
  },
1899
2375
  )) {
@@ -1929,6 +2405,7 @@ export class FanoutSolver extends BaseSolver {
1929
2405
  }
1930
2406
  fixedViaPointsByConnectionIndex = candidateFixedViaPoints
1931
2407
  if (yield* routeMatchedBoundaryBusSteps(candidateBus)) {
2408
+ debugDense("lookahead:start", candidateBus.busId)
1932
2409
  const candidateLeavesAFeasibleExtension =
1933
2410
  laterBuses.length === 0 ||
1934
2411
  laterBuses.some((laterBus) => {
@@ -1941,7 +2418,7 @@ export class FanoutSolver extends BaseSolver {
1941
2418
  return Boolean(
1942
2419
  matchComponentDogboneViaSites(
1943
2420
  [
1944
- ...planeBuses,
2421
+ ...activeBoundaryReservationPlaneBuses,
1945
2422
  ...routedBoundaryBuses,
1946
2423
  candidateBus,
1947
2424
  laterBus,
@@ -1957,14 +2434,23 @@ export class FanoutSolver extends BaseSolver {
1957
2434
  fixedViaPointsByConnectionIndex:
1958
2435
  fixedViaPointsByConnectionIndex,
1959
2436
  blockingSegments: lookaheadBlockingSegments,
2437
+ additionalObstacles: denseAdditionalObstacles,
2438
+ preferPlaneCheckerboardSites:
2439
+ useConfiguredDensePlaneRouting,
1960
2440
  canShareCopper,
1961
2441
  },
1962
2442
  ),
1963
2443
  )
1964
2444
  })
2445
+ debugDense(
2446
+ "lookahead:complete",
2447
+ candidateBus.busId,
2448
+ candidateLeavesAFeasibleExtension,
2449
+ )
1965
2450
  if (candidateLeavesAFeasibleExtension) {
1966
2451
  selectedBusIndex = candidateIndex
1967
2452
  routedBoundaryBuses.push(candidateBus)
2453
+ reserveAllPlaneDogbonesAfterFirstWideBus(candidateBus)
1968
2454
  this.setInProgressPlans({
1969
2455
  phase: "route-dense-boundary-buses",
1970
2456
  plans: matchedPlans,
@@ -1995,12 +2481,15 @@ export class FanoutSolver extends BaseSolver {
1995
2481
  }
1996
2482
  remainingBoundaryBuses.splice(selectedBusIndex, 1)
1997
2483
  }
2484
+ let matchedPlaneBusesInRoutingOrder: PreparedBus[] | null = null
1998
2485
  if (matchedRoutingSucceeded) {
1999
2486
  let feasibleViaPoints: Map<number, { x: number; y: number }> | null =
2000
2487
  null
2488
+ let feasibleAlternatePlanePlans: FanoutRoutePlan[] = []
2001
2489
  const matchViaPointsAroundPlans = (
2002
2490
  candidatePlans: readonly FanoutRoutePlan[],
2003
2491
  ): Map<number, { x: number; y: number }> | null => {
2492
+ feasibleAlternatePlanePlans = []
2004
2493
  const fixedBoundaryViaPoints = new Map(
2005
2494
  candidatePlans.flatMap((plan) =>
2006
2495
  plan.via
@@ -2008,6 +2497,765 @@ export class FanoutSolver extends BaseSolver {
2008
2497
  : [],
2009
2498
  ),
2010
2499
  )
2500
+ const blockingSegments = candidatePlans.flatMap((plan) =>
2501
+ plan.segments.map((segment) => ({
2502
+ connectionIndex: plan.connectionIndex,
2503
+ segment,
2504
+ })),
2505
+ )
2506
+ if (
2507
+ useConfiguredDensePlaneRouting ||
2508
+ process.env.FANOUT_DEBUG_INCREMENTAL_PLANE_MATCH === "1"
2509
+ ) {
2510
+ let incrementalViaPoints = new Map(fixedBoundaryViaPoints)
2511
+ const matchedPlaneBuses = [...activeBoundaryReservationPlaneBuses]
2512
+ for (const planeBus of matchedPlaneBuses) {
2513
+ for (const connection of planeBus.connections) {
2514
+ const reservedPoint = fixedViaPointsByConnectionIndex.get(
2515
+ connection.connectionIndex,
2516
+ )
2517
+ if (reservedPoint) {
2518
+ incrementalViaPoints.set(
2519
+ connection.connectionIndex,
2520
+ reservedPoint,
2521
+ )
2522
+ }
2523
+ }
2524
+ }
2525
+ for (const planeBus of matchedPlaneBuses) {
2526
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
2527
+ if (!targetLayer) return null
2528
+ const reservedPlanePlans = routeBus({
2529
+ srj: this.routingSrj,
2530
+ bus: planeBus,
2531
+ targetLayer,
2532
+ acceptedPlans: [
2533
+ ...candidatePlans,
2534
+ ...feasibleAlternatePlanePlans,
2535
+ ],
2536
+ layerNames: this.config.layerNames,
2537
+ traceWidth: this.config.traceWidth,
2538
+ viaDiameter: this.config.viaDiameter,
2539
+ viaHoleDiameter: this.config.viaHoleDiameter,
2540
+ clearance: this.config.clearance,
2541
+ compactBusTracks: this.config.compactBusTracks,
2542
+ allowBlindAndBuriedVias: false,
2543
+ allowSameNetMerges: this.config.allowSameNetMerges,
2544
+ staticClearanceCache: this.routeStaticClearanceCache,
2545
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
2546
+ })
2547
+ if (!reservedPlanePlans) return null
2548
+ feasibleAlternatePlanePlans.push(...reservedPlanePlans)
2549
+ }
2550
+ const independentlyUnmatchablePlaneBuses = planeBuses.filter(
2551
+ (planeBus) =>
2552
+ !matchedPlaneBuses.includes(planeBus) &&
2553
+ !matchComponentDogboneViaSites(
2554
+ [...matchedPlaneBuses, planeBus, ...boundaryBuses],
2555
+ {
2556
+ viaDiameter: this.config.viaDiameter,
2557
+ viaHoleDiameter: this.config.viaHoleDiameter,
2558
+ traceWidth: this.config.traceWidth,
2559
+ clearance: this.config.clearance,
2560
+ maximumSearchStates: 100_000,
2561
+ preferredBoundaryPerpendicularSideByBusId,
2562
+ preferBoundaryOutwardByBusId,
2563
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
2564
+ blockingSegments,
2565
+ additionalObstacles: denseAdditionalObstacles,
2566
+ preferPlaneCheckerboardSites:
2567
+ useConfiguredDensePlaneRouting,
2568
+ canShareCopper,
2569
+ },
2570
+ ),
2571
+ )
2572
+ const shallowestPlaneLayerIndex = Math.min(
2573
+ ...planeBuses.map((bus) =>
2574
+ this.config.layerNames.indexOf(
2575
+ params.busLayerAssignments[bus.busId] ?? "",
2576
+ ),
2577
+ ),
2578
+ )
2579
+ const deeperPlaneBuses =
2580
+ useConfiguredDensePlaneRouting ||
2581
+ process.env.FANOUT_DEBUG_ROUTE_DEEP_PLANES_FIRST === "1"
2582
+ ? planeBuses.filter(
2583
+ (bus) =>
2584
+ !matchedPlaneBuses.includes(bus) &&
2585
+ this.config.layerNames.indexOf(
2586
+ params.busLayerAssignments[bus.busId] ?? "",
2587
+ ) > shallowestPlaneLayerIndex,
2588
+ )
2589
+ : []
2590
+ const additionalAlternatePlaneBusIds = new Set([
2591
+ ...this.config.denseUnrestrictedPlaneRoutingBusIds,
2592
+ ...(process.env.FANOUT_DEBUG_ADDITIONAL_ALTERNATE_PLANE_BUS_IDS?.split(
2593
+ ",",
2594
+ ) ?? []),
2595
+ ])
2596
+ const additionalAlternatePlaneBuses = planeBuses.filter(
2597
+ (bus) =>
2598
+ !matchedPlaneBuses.includes(bus) &&
2599
+ additionalAlternatePlaneBusIds.has(bus.busId),
2600
+ )
2601
+ const alternatePlaneBuses = [
2602
+ ...deeperPlaneBuses,
2603
+ ...independentlyUnmatchablePlaneBuses.filter(
2604
+ (bus) => !deeperPlaneBuses.includes(bus),
2605
+ ),
2606
+ ...additionalAlternatePlaneBuses.filter(
2607
+ (bus) =>
2608
+ !deeperPlaneBuses.includes(bus) &&
2609
+ !independentlyUnmatchablePlaneBuses.includes(bus),
2610
+ ),
2611
+ ]
2612
+ const debugAlternatePlaneOrder =
2613
+ process.env.FANOUT_DEBUG_ALTERNATE_PLANE_ORDER?.split(",") ?? []
2614
+ const orderedAlternatePlaneBuses = alternatePlaneBuses.toSorted(
2615
+ (first, second) => {
2616
+ const firstLayerIndex = this.config.layerNames.indexOf(
2617
+ params.busLayerAssignments[first.busId] ?? "",
2618
+ )
2619
+ const secondLayerIndex = this.config.layerNames.indexOf(
2620
+ params.busLayerAssignments[second.busId] ?? "",
2621
+ )
2622
+ if (
2623
+ firstLayerIndex !== secondLayerIndex &&
2624
+ process.env.FANOUT_DEBUG_ALTERNATE_IGNORE_LAYERS !== "1"
2625
+ ) {
2626
+ return process.env.FANOUT_DEBUG_ALTERNATE_SHALLOW_FIRST ===
2627
+ "1"
2628
+ ? firstLayerIndex - secondLayerIndex
2629
+ : secondLayerIndex - firstLayerIndex
2630
+ }
2631
+ const firstPriority = debugAlternatePlaneOrder.indexOf(
2632
+ first.busId,
2633
+ )
2634
+ const secondPriority = debugAlternatePlaneOrder.indexOf(
2635
+ second.busId,
2636
+ )
2637
+ return (
2638
+ (firstPriority < 0
2639
+ ? debugAlternatePlaneOrder.length
2640
+ : firstPriority) -
2641
+ (secondPriority < 0
2642
+ ? debugAlternatePlaneOrder.length
2643
+ : secondPriority) ||
2644
+ first.connections[0]!.connectionIndex -
2645
+ second.connections[0]!.connectionIndex
2646
+ )
2647
+ },
2648
+ )
2649
+ if (alternatePlaneBuses.length > 0) {
2650
+ debugDense(
2651
+ "plane-match:preflight-failed",
2652
+ independentlyUnmatchablePlaneBuses.map((bus) => bus.busId),
2653
+ )
2654
+ if (
2655
+ !useConfiguredDensePlaneRouting &&
2656
+ process.env.FANOUT_DEBUG_ROUTE_UNMATCHED_PLANES !== "1"
2657
+ ) {
2658
+ return null
2659
+ }
2660
+ let alternatePlaneSearchStates = 0
2661
+ const maximumAlternatePlaneSearchStates = Number(
2662
+ process.env.FANOUT_DEBUG_ALTERNATE_SEARCH_STATES ??
2663
+ (useConfiguredDensePlaneRouting ? 3_000_000 : 1_000),
2664
+ )
2665
+ const maximumAlternatePlaneRoutes = Number(
2666
+ process.env.FANOUT_DEBUG_ALTERNATE_ROUTE_COUNT ??
2667
+ (useConfiguredDensePlaneRouting ? 128 : 8),
2668
+ )
2669
+ let deepestAlternatePlaneSearchIndex = 0
2670
+ const alternatePlaneFailureCountByBusId = new Map<
2671
+ string,
2672
+ number
2673
+ >()
2674
+ const getPlaneRouteAlternatives = (
2675
+ planeBus: PreparedBus,
2676
+ additionalAcceptedPlans: FanoutRoutePlan[],
2677
+ maximumRoutes = maximumAlternatePlaneRoutes,
2678
+ ): FanoutRoutePlan[][] => {
2679
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
2680
+ if (!targetLayer) return []
2681
+ return routeBusAlternatives(
2682
+ {
2683
+ srj: this.routingSrj,
2684
+ bus: planeBus,
2685
+ targetLayer,
2686
+ acceptedPlans: [
2687
+ ...candidatePlans,
2688
+ ...additionalAcceptedPlans,
2689
+ ],
2690
+ layerNames: this.config.layerNames,
2691
+ traceWidth: this.config.traceWidth,
2692
+ viaDiameter: this.config.viaDiameter,
2693
+ viaHoleDiameter: this.config.viaHoleDiameter,
2694
+ clearance: this.config.clearance,
2695
+ compactBusTracks: this.config.compactBusTracks,
2696
+ allowBlindAndBuriedVias: false,
2697
+ allowSameNetMerges: this.config.allowSameNetMerges,
2698
+ staticClearanceCache: this.routeStaticClearanceCache,
2699
+ },
2700
+ maximumRoutes,
2701
+ )
2702
+ }
2703
+ const routeAlternatePlaneBuses = (
2704
+ remainingPlaneBuses: PreparedBus[],
2705
+ acceptedAlternatePlans: FanoutRoutePlan[],
2706
+ ): FanoutRoutePlan[] | null => {
2707
+ if (remainingPlaneBuses.length === 0) {
2708
+ return acceptedAlternatePlans
2709
+ }
2710
+ if (
2711
+ alternatePlaneSearchStates >=
2712
+ maximumAlternatePlaneSearchStates
2713
+ ) {
2714
+ return null
2715
+ }
2716
+ deepestAlternatePlaneSearchIndex = Math.max(
2717
+ deepestAlternatePlaneSearchIndex,
2718
+ orderedAlternatePlaneBuses.length -
2719
+ remainingPlaneBuses.length,
2720
+ )
2721
+ const alternativesByBus = remainingPlaneBuses.map(
2722
+ (planeBus) => ({
2723
+ planeBus,
2724
+ alternatives: getPlaneRouteAlternatives(
2725
+ planeBus,
2726
+ acceptedAlternatePlans,
2727
+ ),
2728
+ }),
2729
+ )
2730
+ const orderedSelections =
2731
+ process.env.FANOUT_DEBUG_DYNAMIC_ALTERNATE_ORDER === "1"
2732
+ ? alternativesByBus.toSorted(
2733
+ (first, second) =>
2734
+ first.alternatives.length -
2735
+ second.alternatives.length ||
2736
+ orderedAlternatePlaneBuses.indexOf(first.planeBus) -
2737
+ orderedAlternatePlaneBuses.indexOf(second.planeBus),
2738
+ )
2739
+ : alternativesByBus
2740
+ const selected = orderedSelections[0]!
2741
+ const { planeBus, alternatives } = selected
2742
+ if (
2743
+ process.env.FANOUT_DEBUG_ALTERNATE_CHOICES === "1" &&
2744
+ alternatePlaneSearchStates < 64
2745
+ ) {
2746
+ debugDense(
2747
+ "plane-route:alternate-choice",
2748
+ `depth:${orderedAlternatePlaneBuses.length - remainingPlaneBuses.length}`,
2749
+ planeBus.busId,
2750
+ alternativesByBus.map((entry) => [
2751
+ entry.planeBus.busId,
2752
+ entry.alternatives.length,
2753
+ ]),
2754
+ )
2755
+ }
2756
+ if (alternatives.length === 0) {
2757
+ alternatePlaneFailureCountByBusId.set(
2758
+ planeBus.busId,
2759
+ (alternatePlaneFailureCountByBusId.get(planeBus.busId) ??
2760
+ 0) + 1,
2761
+ )
2762
+ return null
2763
+ }
2764
+ const selectionsToSearch =
2765
+ process.env.FANOUT_DEBUG_BRANCH_ALTERNATE_ORDER === "1"
2766
+ ? orderedSelections
2767
+ : [selected]
2768
+ const alternateActions = selectionsToSearch.flatMap(
2769
+ (selection) =>
2770
+ selection.alternatives.map((alternative) => ({
2771
+ selection,
2772
+ alternative,
2773
+ remaining: remainingPlaneBuses.filter(
2774
+ (candidate) => candidate !== selection.planeBus,
2775
+ ),
2776
+ })),
2777
+ )
2778
+ const orderedActions =
2779
+ process.env.FANOUT_DEBUG_LEAST_CONSTRAINING_ALTERNATES === "1"
2780
+ ? alternateActions
2781
+ .map((action) => {
2782
+ const acceptedPlans = [
2783
+ ...acceptedAlternatePlans,
2784
+ ...action.alternative,
2785
+ ]
2786
+ const remainingOptionCounts = action.remaining.map(
2787
+ (remainingBus) =>
2788
+ getPlaneRouteAlternatives(
2789
+ remainingBus,
2790
+ acceptedPlans,
2791
+ Math.min(4, maximumAlternatePlaneRoutes),
2792
+ ).length,
2793
+ )
2794
+ return {
2795
+ ...action,
2796
+ remainingOptionCounts,
2797
+ minimumRemainingOptions:
2798
+ remainingOptionCounts.length === 0
2799
+ ? Number.POSITIVE_INFINITY
2800
+ : Math.min(...remainingOptionCounts),
2801
+ totalRemainingOptions: remainingOptionCounts.reduce(
2802
+ (total, count) => total + count,
2803
+ 0,
2804
+ ),
2805
+ }
2806
+ })
2807
+ .filter(
2808
+ (action) => action.minimumRemainingOptions !== 0,
2809
+ )
2810
+ .toSorted(
2811
+ (first, second) =>
2812
+ second.minimumRemainingOptions -
2813
+ first.minimumRemainingOptions ||
2814
+ second.totalRemainingOptions -
2815
+ first.totalRemainingOptions,
2816
+ )
2817
+ : alternateActions
2818
+ for (const action of orderedActions) {
2819
+ alternatePlaneSearchStates++
2820
+ const completedPlans = routeAlternatePlaneBuses(
2821
+ action.remaining,
2822
+ [...acceptedAlternatePlans, ...action.alternative],
2823
+ )
2824
+ if (completedPlans) return completedPlans
2825
+ if (
2826
+ alternatePlaneSearchStates >=
2827
+ maximumAlternatePlaneSearchStates
2828
+ ) {
2829
+ break
2830
+ }
2831
+ }
2832
+ alternatePlaneFailureCountByBusId.set(
2833
+ planeBus.busId,
2834
+ (alternatePlaneFailureCountByBusId.get(planeBus.busId) ?? 0) +
2835
+ 1,
2836
+ )
2837
+ return null
2838
+ }
2839
+ let alternatePlanePlans: FanoutRoutePlan[] | null
2840
+ if (
2841
+ useConfiguredDensePlaneRouting ||
2842
+ process.env.FANOUT_DEBUG_EXACT_COVER_ALTERNATES === "1"
2843
+ ) {
2844
+ type IndependentPlaneRouteCandidate = {
2845
+ key: string
2846
+ planeBus: PreparedBus
2847
+ plans: FanoutRoutePlan[]
2848
+ }
2849
+ const candidateSets = orderedAlternatePlaneBuses.map(
2850
+ (planeBus) => ({
2851
+ planeBus,
2852
+ candidates: getPlaneRouteAlternatives(
2853
+ planeBus,
2854
+ feasibleAlternatePlanePlans,
2855
+ ).map((plans, index) => ({
2856
+ key: `${planeBus.busId}:${index}`,
2857
+ planeBus,
2858
+ plans,
2859
+ })),
2860
+ }),
2861
+ )
2862
+ debugDense(
2863
+ "plane-route:alternate-candidate-counts",
2864
+ candidateSets.map((candidateSet) => [
2865
+ candidateSet.planeBus.busId,
2866
+ candidateSet.candidates.length,
2867
+ ]),
2868
+ )
2869
+ const compatibilityByCandidatePair = new Map<string, boolean>()
2870
+ const candidatesAreCompatible = (
2871
+ first: IndependentPlaneRouteCandidate,
2872
+ second: IndependentPlaneRouteCandidate,
2873
+ ): boolean => {
2874
+ const cacheKey = [first.key, second.key].toSorted().join("|")
2875
+ const cached = compatibilityByCandidatePair.get(cacheKey)
2876
+ if (cached !== undefined) return cached
2877
+ const compatible = fanoutPlansAreMutuallyClear({
2878
+ plans: [...first.plans, ...second.plans],
2879
+ srj: this.routingSrj,
2880
+ clearance: this.config.clearance,
2881
+ allowSameNetMerges: this.config.allowSameNetMerges,
2882
+ })
2883
+ compatibilityByCandidatePair.set(cacheKey, compatible)
2884
+ return compatible
2885
+ }
2886
+ const selectCompatiblePlaneRoutes = (
2887
+ remainingCandidateSets: typeof candidateSets,
2888
+ selectedCandidates: IndependentPlaneRouteCandidate[],
2889
+ ): IndependentPlaneRouteCandidate[] | null => {
2890
+ if (remainingCandidateSets.length === 0) {
2891
+ return selectedCandidates
2892
+ }
2893
+ if (
2894
+ alternatePlaneSearchStates >=
2895
+ maximumAlternatePlaneSearchStates
2896
+ ) {
2897
+ return null
2898
+ }
2899
+ deepestAlternatePlaneSearchIndex = Math.max(
2900
+ deepestAlternatePlaneSearchIndex,
2901
+ orderedAlternatePlaneBuses.length -
2902
+ remainingCandidateSets.length,
2903
+ )
2904
+ const selectedSet = remainingCandidateSets.toSorted(
2905
+ (first, second) =>
2906
+ first.candidates.length - second.candidates.length,
2907
+ )[0]!
2908
+ if (selectedSet.candidates.length === 0) {
2909
+ alternatePlaneFailureCountByBusId.set(
2910
+ selectedSet.planeBus.busId,
2911
+ (alternatePlaneFailureCountByBusId.get(
2912
+ selectedSet.planeBus.busId,
2913
+ ) ?? 0) + 1,
2914
+ )
2915
+ return null
2916
+ }
2917
+ const otherSets = remainingCandidateSets.filter(
2918
+ (candidateSet) => candidateSet !== selectedSet,
2919
+ )
2920
+ const candidateBatchSize = Number(
2921
+ process.env.FANOUT_DEBUG_ALTERNATE_CANDIDATE_BATCH_SIZE ??
2922
+ 16,
2923
+ )
2924
+ for (
2925
+ let batchStart = 0;
2926
+ batchStart < selectedSet.candidates.length;
2927
+ batchStart += candidateBatchSize
2928
+ ) {
2929
+ const actions = selectedSet.candidates
2930
+ .slice(batchStart, batchStart + candidateBatchSize)
2931
+ .map((candidate) => {
2932
+ const projectedSets = otherSets.map((candidateSet) => ({
2933
+ ...candidateSet,
2934
+ candidates: candidateSet.candidates.filter(
2935
+ (otherCandidate) =>
2936
+ candidatesAreCompatible(
2937
+ candidate,
2938
+ otherCandidate,
2939
+ ),
2940
+ ),
2941
+ }))
2942
+ const projectedCounts = projectedSets.map(
2943
+ (candidateSet) => candidateSet.candidates.length,
2944
+ )
2945
+ return {
2946
+ candidate,
2947
+ projectedSets,
2948
+ minimumProjectedCount:
2949
+ projectedCounts.length === 0
2950
+ ? Number.POSITIVE_INFINITY
2951
+ : Math.min(...projectedCounts),
2952
+ totalProjectedCount: projectedCounts.reduce(
2953
+ (total, count) => total + count,
2954
+ 0,
2955
+ ),
2956
+ }
2957
+ })
2958
+ .filter((action) => action.minimumProjectedCount !== 0)
2959
+ .toSorted(
2960
+ (first, second) =>
2961
+ second.minimumProjectedCount -
2962
+ first.minimumProjectedCount ||
2963
+ second.totalProjectedCount -
2964
+ first.totalProjectedCount,
2965
+ )
2966
+ for (const action of actions) {
2967
+ alternatePlaneSearchStates++
2968
+ const selected = selectCompatiblePlaneRoutes(
2969
+ action.projectedSets,
2970
+ [...selectedCandidates, action.candidate],
2971
+ )
2972
+ if (selected) return selected
2973
+ if (
2974
+ alternatePlaneSearchStates >=
2975
+ maximumAlternatePlaneSearchStates
2976
+ ) {
2977
+ break
2978
+ }
2979
+ }
2980
+ if (
2981
+ alternatePlaneSearchStates >=
2982
+ maximumAlternatePlaneSearchStates
2983
+ ) {
2984
+ break
2985
+ }
2986
+ }
2987
+ alternatePlaneFailureCountByBusId.set(
2988
+ selectedSet.planeBus.busId,
2989
+ (alternatePlaneFailureCountByBusId.get(
2990
+ selectedSet.planeBus.busId,
2991
+ ) ?? 0) + 1,
2992
+ )
2993
+ return null
2994
+ }
2995
+ const selectedCandidates = selectCompatiblePlaneRoutes(
2996
+ candidateSets,
2997
+ [],
2998
+ )
2999
+ alternatePlanePlans = selectedCandidates
3000
+ ? [
3001
+ ...feasibleAlternatePlanePlans,
3002
+ ...selectedCandidates.flatMap(
3003
+ (candidate) => candidate.plans,
3004
+ ),
3005
+ ]
3006
+ : null
3007
+ } else {
3008
+ alternatePlanePlans = routeAlternatePlaneBuses(
3009
+ orderedAlternatePlaneBuses,
3010
+ feasibleAlternatePlanePlans,
3011
+ )
3012
+ }
3013
+ debugDense(
3014
+ "plane-route:alternate-search",
3015
+ alternatePlanePlans ? "complete" : "failed",
3016
+ alternatePlaneSearchStates,
3017
+ `depth:${deepestAlternatePlaneSearchIndex}/${orderedAlternatePlaneBuses.length}`,
3018
+ [...alternatePlaneFailureCountByBusId].toSorted(
3019
+ ([, first], [, second]) => second - first,
3020
+ ),
3021
+ )
3022
+ if (!alternatePlanePlans) return null
3023
+ feasibleAlternatePlanePlans = alternatePlanePlans
3024
+ }
3025
+ let planeBusIdsRoutedWithoutDogbones = new Set<string>()
3026
+ let allBlockingSegments = [...blockingSegments]
3027
+ let alternateBlockingVias: {
3028
+ connectionIndex: number
3029
+ center: { x: number; y: number }
3030
+ diameter: number
3031
+ spanLayers: readonly string[]
3032
+ }[] = []
3033
+ let planeBusesToMatch = [...planeBuses]
3034
+ let candidateCountByConnectionIndex = new Map<number, number>()
3035
+ let candidatePointsByConnectionIndex = new Map<
3036
+ number,
3037
+ { x: number; y: number }[]
3038
+ >()
3039
+ const refreshPlaneDogboneCandidates = (): void => {
3040
+ planeBusIdsRoutedWithoutDogbones = new Set(
3041
+ feasibleAlternatePlanePlans.map((plan) => plan.busId),
3042
+ )
3043
+ const alternateBlockingSegments =
3044
+ feasibleAlternatePlanePlans.flatMap((plan) =>
3045
+ [...plan.segments, ...(plan.planeEndpointSegments ?? [])].map(
3046
+ (segment) => ({
3047
+ connectionIndex: plan.connectionIndex,
3048
+ segment,
3049
+ }),
3050
+ ),
3051
+ )
3052
+ allBlockingSegments = [
3053
+ ...blockingSegments,
3054
+ ...alternateBlockingSegments,
3055
+ ]
3056
+ alternateBlockingVias = feasibleAlternatePlanePlans.flatMap(
3057
+ (plan) =>
3058
+ [
3059
+ plan.via,
3060
+ ...(plan.additionalVias ?? []),
3061
+ plan.planeEndpointVia,
3062
+ ].flatMap((via) =>
3063
+ via
3064
+ ? [
3065
+ {
3066
+ connectionIndex: plan.connectionIndex,
3067
+ center: via.center,
3068
+ diameter: via.diameter,
3069
+ spanLayers: via.spanLayers,
3070
+ },
3071
+ ]
3072
+ : [],
3073
+ ),
3074
+ )
3075
+ planeBusesToMatch = planeBuses.filter(
3076
+ (bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId),
3077
+ )
3078
+ candidateCountByConnectionIndex = new Map()
3079
+ candidatePointsByConnectionIndex = new Map()
3080
+ for (const candidate of getComponentDogboneViaSiteCandidates(
3081
+ planeBusesToMatch,
3082
+ {
3083
+ viaDiameter: this.config.viaDiameter,
3084
+ viaHoleDiameter: this.config.viaHoleDiameter,
3085
+ traceWidth: this.config.traceWidth,
3086
+ clearance: this.config.clearance,
3087
+ blockingSegments: allBlockingSegments,
3088
+ blockingVias: alternateBlockingVias,
3089
+ additionalObstacles: denseAdditionalObstacles,
3090
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3091
+ canShareCopper,
3092
+ },
3093
+ )) {
3094
+ candidateCountByConnectionIndex.set(
3095
+ candidate.connectionIndex,
3096
+ (candidateCountByConnectionIndex.get(
3097
+ candidate.connectionIndex,
3098
+ ) ?? 0) + 1,
3099
+ )
3100
+ const points =
3101
+ candidatePointsByConnectionIndex.get(
3102
+ candidate.connectionIndex,
3103
+ ) ?? []
3104
+ points.push(candidate.point)
3105
+ candidatePointsByConnectionIndex.set(
3106
+ candidate.connectionIndex,
3107
+ points,
3108
+ )
3109
+ }
3110
+ }
3111
+ refreshPlaneDogboneCandidates()
3112
+ for (
3113
+ let promotionPass = 0;
3114
+ promotionPass < planeBuses.length;
3115
+ promotionPass++
3116
+ ) {
3117
+ const zeroCandidatePlaneBuses = planeBusesToMatch.filter(
3118
+ (bus) =>
3119
+ !candidateCountByConnectionIndex.has(
3120
+ bus.connections[0]!.connectionIndex,
3121
+ ),
3122
+ )
3123
+ if (zeroCandidatePlaneBuses.length === 0) break
3124
+ debugDense(
3125
+ "plane-route:promote-zero-candidates",
3126
+ zeroCandidatePlaneBuses.map((bus) => bus.busId),
3127
+ )
3128
+ if (
3129
+ zeroCandidatePlaneBuses.some((bus) =>
3130
+ matchedPlaneBuses.includes(bus),
3131
+ )
3132
+ ) {
3133
+ return null
3134
+ }
3135
+ for (const planeBus of zeroCandidatePlaneBuses) {
3136
+ const targetLayer = params.busLayerAssignments[planeBus.busId]
3137
+ if (!targetLayer) return null
3138
+ const promotedPlans = routeBusAlternatives(
3139
+ {
3140
+ srj: this.routingSrj,
3141
+ bus: planeBus,
3142
+ targetLayer,
3143
+ acceptedPlans: [
3144
+ ...candidatePlans,
3145
+ ...feasibleAlternatePlanePlans,
3146
+ ],
3147
+ layerNames: this.config.layerNames,
3148
+ traceWidth: this.config.traceWidth,
3149
+ viaDiameter: this.config.viaDiameter,
3150
+ viaHoleDiameter: this.config.viaHoleDiameter,
3151
+ clearance: this.config.clearance,
3152
+ compactBusTracks: this.config.compactBusTracks,
3153
+ allowBlindAndBuriedVias: false,
3154
+ allowSameNetMerges: this.config.allowSameNetMerges,
3155
+ staticClearanceCache: this.routeStaticClearanceCache,
3156
+ },
3157
+ 8,
3158
+ )[0]
3159
+ if (!promotedPlans) {
3160
+ debugDense("plane-route:promote-failed", planeBus.busId)
3161
+ return null
3162
+ }
3163
+ feasibleAlternatePlanePlans.push(...promotedPlans)
3164
+ }
3165
+ refreshPlaneDogboneCandidates()
3166
+ }
3167
+ const debugPlaneMatchOrder =
3168
+ process.env.FANOUT_DEBUG_PLANE_MATCH_ORDER?.split(",") ?? []
3169
+ const incrementalPlaneBuses = planeBusesToMatch.toSorted(
3170
+ (first, second) => {
3171
+ const candidateCountDifference =
3172
+ (candidateCountByConnectionIndex.get(
3173
+ first.connections[0]!.connectionIndex,
3174
+ ) ?? 0) -
3175
+ (candidateCountByConnectionIndex.get(
3176
+ second.connections[0]!.connectionIndex,
3177
+ ) ?? 0)
3178
+ if (candidateCountDifference !== 0) {
3179
+ return candidateCountDifference
3180
+ }
3181
+ const firstPriority = debugPlaneMatchOrder.indexOf(first.busId)
3182
+ const secondPriority = debugPlaneMatchOrder.indexOf(
3183
+ second.busId,
3184
+ )
3185
+ const priorityDifference =
3186
+ (firstPriority < 0
3187
+ ? debugPlaneMatchOrder.length
3188
+ : firstPriority) -
3189
+ (secondPriority < 0
3190
+ ? debugPlaneMatchOrder.length
3191
+ : secondPriority)
3192
+ if (priorityDifference !== 0) return priorityDifference
3193
+ return (
3194
+ first.connections[0]!.connectionIndex -
3195
+ second.connections[0]!.connectionIndex
3196
+ )
3197
+ },
3198
+ )
3199
+ for (const planeBus of incrementalPlaneBuses) {
3200
+ if (matchedPlaneBuses.includes(planeBus)) continue
3201
+ if (
3202
+ process.env.FANOUT_DEBUG_PLANE_CANDIDATES?.split(",").includes(
3203
+ planeBus.busId,
3204
+ )
3205
+ ) {
3206
+ debugDense(
3207
+ "plane-match:candidates",
3208
+ planeBus.busId,
3209
+ candidatePointsByConnectionIndex.get(
3210
+ planeBus.connections[0]!.connectionIndex,
3211
+ ),
3212
+ )
3213
+ }
3214
+ const nextViaPoints = matchComponentDogboneViaSites(
3215
+ [...matchedPlaneBuses, planeBus, ...boundaryBuses],
3216
+ {
3217
+ viaDiameter: this.config.viaDiameter,
3218
+ viaHoleDiameter: this.config.viaHoleDiameter,
3219
+ traceWidth: this.config.traceWidth,
3220
+ clearance: this.config.clearance,
3221
+ maximumSearchStates: 100_000,
3222
+ preferredBoundaryPerpendicularSideByBusId,
3223
+ preferBoundaryOutwardByBusId,
3224
+ fixedViaPointsByConnectionIndex: incrementalViaPoints,
3225
+ blockingSegments: allBlockingSegments,
3226
+ blockingVias: alternateBlockingVias,
3227
+ additionalObstacles: denseAdditionalObstacles,
3228
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
3229
+ canShareCopper,
3230
+ },
3231
+ )
3232
+ debugDense(
3233
+ nextViaPoints
3234
+ ? "plane-match:incremental"
3235
+ : "plane-match:incremental-failed",
3236
+ planeBus.busId,
3237
+ candidateCountByConnectionIndex.get(
3238
+ planeBus.connections[0]!.connectionIndex,
3239
+ ) ?? 0,
3240
+ nextViaPoints?.get(planeBus.connections[0]!.connectionIndex),
3241
+ nextViaPoints?.size ?? "failed",
3242
+ )
3243
+ if (!nextViaPoints) return null
3244
+ incrementalViaPoints = new Map([
3245
+ ...incrementalViaPoints,
3246
+ ...nextViaPoints,
3247
+ ])
3248
+ matchedPlaneBuses.push(planeBus)
3249
+ }
3250
+ matchedPlaneBusesInRoutingOrder = matchedPlaneBuses.filter(
3251
+ (bus) => !planeBusIdsRoutedWithoutDogbones.has(bus.busId),
3252
+ )
3253
+ debugDense(
3254
+ "plane-match:incremental-complete",
3255
+ incrementalViaPoints.size,
3256
+ )
3257
+ return incrementalViaPoints
3258
+ }
2011
3259
  return matchComponentDogboneViaSites(
2012
3260
  [...planeBuses, ...boundaryBuses],
2013
3261
  {
@@ -2019,16 +3267,14 @@ export class FanoutSolver extends BaseSolver {
2019
3267
  preferredBoundaryPerpendicularSideByBusId,
2020
3268
  preferBoundaryOutwardByBusId,
2021
3269
  fixedViaPointsByConnectionIndex: fixedBoundaryViaPoints,
2022
- blockingSegments: candidatePlans.flatMap((plan) =>
2023
- plan.segments.map((segment) => ({
2024
- connectionIndex: plan.connectionIndex,
2025
- segment,
2026
- })),
2027
- ),
3270
+ blockingSegments,
3271
+ additionalObstacles: denseAdditionalObstacles,
3272
+ preferPlaneCheckerboardSites: useConfiguredDensePlaneRouting,
2028
3273
  canShareCopper,
2029
3274
  },
2030
3275
  )
2031
3276
  }
3277
+ debugDense("length-match:start", matchedPlans.length)
2032
3278
  const matchedLengthResult = matchBusPlanLengths({
2033
3279
  plans: matchedPlans,
2034
3280
  preparedBuses: this.preparedBuses,
@@ -2045,6 +3291,10 @@ export class FanoutSolver extends BaseSolver {
2045
3291
  return true
2046
3292
  },
2047
3293
  })
3294
+ debugDense(
3295
+ "length-match:complete",
3296
+ matchedLengthResult.plans?.length ?? "failed",
3297
+ )
2048
3298
  this.setInProgressPlans({
2049
3299
  phase: "match-dense-boundary-lengths",
2050
3300
  plans: matchedLengthResult.plans ?? matchedPlans,
@@ -2059,6 +3309,7 @@ export class FanoutSolver extends BaseSolver {
2059
3309
  feasibleViaPoints ?? matchViaPointsAroundPlans(matchedPlans)
2060
3310
  if (rematchedViaPoints) {
2061
3311
  fixedViaPointsByConnectionIndex = rematchedViaPoints
3312
+ matchedPlans.push(...feasibleAlternatePlanePlans)
2062
3313
  } else {
2063
3314
  matchedRoutingSucceeded = false
2064
3315
  }
@@ -2075,8 +3326,10 @@ export class FanoutSolver extends BaseSolver {
2075
3326
  yield
2076
3327
  }
2077
3328
  if (matchedRoutingSucceeded) {
2078
- for (const bus of planeBuses) {
3329
+ for (const bus of matchedPlaneBusesInRoutingOrder ?? planeBuses) {
3330
+ debugDense("plane-route:start", bus.busId)
2079
3331
  const targetLayer = params.busLayerAssignments[bus.busId]
3332
+ const blockingBusCounts = new Map<string, number>()
2080
3333
  const busPlans = targetLayer
2081
3334
  ? routeBus({
2082
3335
  srj: this.routingSrj,
@@ -2092,14 +3345,24 @@ export class FanoutSolver extends BaseSolver {
2092
3345
  allowBlindAndBuriedVias: false,
2093
3346
  allowSameNetMerges: this.config.allowSameNetMerges,
2094
3347
  staticClearanceCache: this.routeStaticClearanceCache,
3348
+ blockingBusCounts,
2095
3349
  fixedViaPointsByConnectionIndex,
2096
3350
  })
2097
3351
  : null
2098
3352
  if (!busPlans) {
3353
+ debugDense(
3354
+ "plane-route:failed",
3355
+ bus.busId,
3356
+ fixedViaPointsByConnectionIndex.get(
3357
+ bus.connections[0]!.connectionIndex,
3358
+ ),
3359
+ [...blockingBusCounts],
3360
+ )
2099
3361
  matchedRoutingSucceeded = false
2100
3362
  break
2101
3363
  }
2102
3364
  matchedPlans.push(...busPlans)
3365
+ debugDense("plane-route:complete", bus.busId)
2103
3366
  this.setInProgressPlans({
2104
3367
  phase: "route-dense-plane-buses",
2105
3368
  plans: matchedPlans,
@@ -2111,7 +3374,7 @@ export class FanoutSolver extends BaseSolver {
2111
3374
  yield
2112
3375
  }
2113
3376
  }
2114
- if (
3377
+ const densePlansAreClear =
2115
3378
  matchedRoutingSucceeded &&
2116
3379
  fanoutPlansAreClear({
2117
3380
  plans: matchedPlans,
@@ -2121,11 +3384,19 @@ export class FanoutSolver extends BaseSolver {
2121
3384
  allowBlindAndBuriedVias: false,
2122
3385
  allowSameNetMerges: this.config.allowSameNetMerges,
2123
3386
  })
2124
- ) {
3387
+ debugDense(
3388
+ "dense-validation",
3389
+ matchedRoutingSucceeded,
3390
+ matchedPlans.length,
3391
+ densePlansAreClear,
3392
+ )
3393
+ if (densePlansAreClear) {
2125
3394
  return { plans: matchedPlans, failedBusIds: [] }
2126
3395
  }
2127
3396
  }
2128
3397
 
3398
+ if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1") return null
3399
+
2129
3400
  const maximumStates = 8
2130
3401
  const getBoundaryStates = (
2131
3402
  alternativesPerBoundaryBus: number,
@@ -2599,6 +3870,17 @@ export class FanoutSolver extends BaseSolver {
2599
3870
  solver: denseSolver,
2600
3871
  }) as MixedTerminationState | null
2601
3872
  }
3873
+ if (
3874
+ !mixedTerminationState &&
3875
+ !useSingleLayerPushAndShove &&
3876
+ routingStrategy === "default" &&
3877
+ process.env.FANOUT_DEBUG_DENSE_ONLY === "1"
3878
+ ) {
3879
+ mixedTerminationState = {
3880
+ plans: [],
3881
+ failedBusIds: this.preparedBuses.map((bus) => bus.busId),
3882
+ }
3883
+ }
2602
3884
 
2603
3885
  if (mixedTerminationState) {
2604
3886
  plans = mixedTerminationState.plans
@@ -2786,6 +4068,7 @@ export class FanoutSolver extends BaseSolver {
2786
4068
  busLayerAssignments,
2787
4069
  "default",
2788
4070
  )
4071
+ if (process.env.FANOUT_DEBUG_DENSE_ONLY === "1") return bestAttempt
2789
4072
  if (
2790
4073
  bestAttempt.summary.routedConnectionCount ===
2791
4074
  this.inputSrj.connections.length &&