π v0.20.0 β planes first: the pour becomes information, and one step owns every piece of copper #580
drandyhaas
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hey everyone! π
v0.20.0 is out, and it's the biggest structural change this router has had. It is worth explaining as an idea rather than a changelog, because almost everything else follows from it.
For most of this project's life, routing a board with power planes went like this: route the signals, pour the planes, then repair the damage. Each step was self-contained and each one made life harder for the next. Signals were routed into space the plane would later want. The pour arrived last and filled whatever was left, getting carved into islands by tracks that never knew it was coming. Then a repair pass went looking for plane pads that ended up connected to nothing, and welded them on β with rip-up authority, after the fact, with less information than the steps that caused the problem.
"Repair" is an honest word for that. It also gives the game away: a pipeline that needs a repair step is one whose earlier steps are allowed to break things.
v0.20.0 reorganizes the chain around a single principle: pour the planes first, treat that pour as information every later step can see, and let one step own every piece of copper the planes imply. There is no repair step any more. There are no taps in the pour step any more. The chain is now:
π¦ How to get it: download KiCadRoutingTools-0.20.0.zip from the Releases page, then in KiCad open Plugin and Content Manager β Install from Fileβ¦ and pick that zip. Prebuilt router binaries for Linux/macOS/Windows are attached β
python build_router.pyfetches them automatically, no Rust toolchain needed. (This release also finally publishes the 0.20.0 Rust binaries. The crate had been at 0.20.0 with nothing published, which quietly meant that anyone without a Rust toolchain couldn't install HEAD at all.)py_router/, so it'spython3 py_router/route.py β¦now (#522).π Why pouring first changes everything downstream
The pour is the single largest piece of copper on most boards. Making it last meant every earlier decision was taken blind to it β and every one of those decisions constrained it.
Pour it first and it stops being an obstacle to be worked around after the fact. It becomes information:
The fanout can see real fill. When the plane already exists, the fanout's plane-drop pass looks at actual filled copper rather than a zone outline. A BGA ball that the pour already covers needs no via at all β the pour touches the pad, and that is the connection. Balls that do need a via get one placed into intact copper, at fanout time, where the escape is being planned anyway. This is what a person does by hand: you look at where the plane actually is, and you only add a via where the plane isn't.
Signal routing can be charged for the damage it does. With a real fill in hand, the plane-fragility field prices every cut through the pour by how narrow the copper is there. Crossing the middle of a wide plane is nearly free; slicing through a 0.3 mm neck that is the only thing connecting two halves of your ground is expensive. Signals therefore cross planes where planes can take it, instead of severing them at their weakest point and leaving a repair pass to discover the wreckage. The pour reflows around tracks; it does not get amputated by them.
Return vias and stitching still come last, deliberately. The Step 1 pour is bare β nets and layers only, no
--add-gnd-vias, no stitching. Those adapt to signals that do not exist yet, and placing them early is how a stitching via ends up sitting in a differential pair's only channel. So they run in a final pass, once the signals are real and the vias can arrange themselves around them.π« The pour step stopped drawing copper β on purpose
This is the change I'd most like to convince you is right, because it looks like a feature removal.
route_planesused to tap. It had a per-pad tap loop, a via-in-pad spiral search, same-net strapping, distant-trace routing, and a rip-and-retry ladder with fallbacks β roughly 520 lines whose job was to reach out from the pour and grab pads that the fill hadn't caught. All of it is deleted, with no kill switch. When we discussed keeping it behind a revert flag, the argument against won: that would leave a second, unexercised way for the pour step to draw copper.That's the principle. One authority draws copper. The pour step now declares intent β this net owns this layer β and fills. Nothing else. Which means:
--rip-blocker-netsis gone from it. (The measured failure mode of ripping there was real: routed signals destroyed to serve tap pads that the fanout's plane drops already covered.)π― The plane as a place to route to, not around
If the pour step no longer reaches out to pads, something has to connect them. That something is the router, and this is the part I find genuinely elegant.
Every pad-covered fill region of a plane now contributes launch cells to the search: real, laddered target cells on the actual filled copper. A pad doesn't get welded to the plane by a later pass β it gets routed to the plane, as a first-class destination, by the same A* that routes everything else.
Two details make it trustworthy rather than approximate:
π§© Everything finishes inside the route step
The last piece: the plane finalize β taps, region joins, cleanup, and a KiCad-verified completion check β now runs inside
batch_route, on both front-ends (the CLI against its output file, the GUI in-process against the live board).Why this is better in principle, and not just fewer commands to type: a separate repair step is a second process reasoning about a board it did not route. It re-parses a written file, rebuilds an obstacle map from scratch, and has no idea which copper the routing step had just placed, ripped, or deliberately protected. It is structurally forced to guess. Running the same work as the run's final phase means it inherits everything: the live obstacle map, custody of every ripped net, the DRC floors actually in force, and the routing scope. And the run commits copper once, at the end, instead of a chain of steps each writing a file the next one must re-interpret.
The rule this creates for you: the finalize covers the pours it knows about, so put the plane nets in your route step's
--nets. A route step that excludes GND excludes GND's pours from its own finalize β by plan, not by accident.repair_planes.pystill exists as a standalone utility (renamed fromroute_disconnected_planes.py) if you want to run it against someone else's board. It is simply no longer a step in the chain.π© Fanout: escapes that are legal by construction
Two fixes here matter more than their size suggests, because both replace assumed legality with checked legality.
Via-in-pad at a ball centre now checks real copper (#567). The centre site used to be granted a blanket pass, which honored the mutual-reservation contract between escape passes but also waived conflicts with copper that was already committed β an earlier pass's escape run may legitimately pass close to a pad centre, since an SMD pad blocks only its own layer. The via was then dropped straight through it (measured intrusions of 0.19β0.24 mm, in both orderings). It now runs the full site-conflict check with the pad-clamped ring and drill, waiving only the reservations that contract actually covers.
A via that doesn't fit no longer kills the net (#568) β which needed a change in the Rust core, and is the subject of the next section.
The same idea now exists at whole-net scale as a last resort: a net that is still open after everything else gets a retry ladder where track width and via size march down together toward the fab floor (clearance never reduced), with no rip authority so it can never take a casualty β and a rung is accepted only if the net then grades fully connected, otherwise it is undone completely. Better a proven-smaller trace than a shipped open.
βοΈ In the Rust core: via size stops being one number for the whole board
Until now the router carried a single via geometry for an entire search. Every grid cell was either via-legal or it wasn't, decided once against that one size. That sounds harmless and isn't, because it makes via size a global commitment made before the router has seen a single obstacle β and dense regions become via-deserts. A net that needed a layer change under a BGA, or between two pads on a tight pitch, simply failed. The only remedies were board-wide and bad: shrink every via (pushing the whole board into a finer, pricier fab class, and derating current on power vias that never needed it), or add layers.
That's not how anyone designs by hand. You use your standard via everywhere, and you drop to a smaller one only in the spots that demand it β under the fine-pitch part, threading a gap. Via size is a property of a site, not of a board.
The Rust core now models it that way (0.20.0). Alongside its via-block map it keeps a second refcounted map at the small fab-rung reserve, and searches can ask
is_via_blocked_rung(cell, rung)β so the same board can be via-illegal at your normal via and legal at the next rung down, cell by cell. On top of that sits the escalation: when an edge would otherwise fail for want of a via, the router samples that edge's neighbourhood on a fine subgrid, exact-validates the smaller via at each blocked cell β per-layer copper clearance plus drill-to-drill spacing against existing board vias and plated through-holes β and re-searches with those cells opened, emitting vias that genuinely are the smaller rung (re-validated again at emission, so what ships is what was proven).Two properties I'd point at as the reason to trust it:
The net effect is that the router gets to answer a local problem locally, instead of failing a net or degrading the whole board. On the three-board check, the boards that had tight spots picked up completions (
ulx3s18 β 16 unrouted with 126 escalation firings,orangecrab18 β 17 with 720) and the clean control board was untouched at zero firings β which is exactly the shape you want: nothing happens where nothing is needed.This is also why this release had to publish new binaries: the
via_rungargument doesn't exist on a 0.19.x.so, so HEAD simply wasn't installable without a Rust toolchain until now. The same crate release also removed the pygame visualizer and its snapshot machinery (#569) β grading, manifest replay and the GUI cover that debugging workflow.π§· Welds you can believe
Two related fixes, in the same spirit of replacing claims with checks.
The KiCad-oracle tier could credit a weld it never made: its escalated strap fell back to routing the two largest fragments in a window, "fixed" an unrelated 0.14 mm gap 7 mm away on the wrong layer, and reported the link welded β after which the same debris re-stacked every round. Copper emitted outside the link's own corridor is now rejected, never credited, and a caller welding a specific pair of points fails honestly when it cannot anchor there.
We also deleted a heuristic that tried to infer a weld from geometry: it treated an exact-fill anchor that happened to coincide with a pre-existing track endpoint as a connection. That's a kiss, not a weld, and it re-cleared the very net the fix existed to route. Geometry doesn't get to overrule a fresh failure.
And when a forced link dies against a genuinely pre-existing wall, its evidence used to be trapped inside a sub-run where the escalation that could act on it never looked. That evidence is now exported, filtered through the same protection rules as everything else (protected nets, negated nets and zone-backed copper are never named), and the run takes another lap with authority to rip the specific blocker β with full custody, so a failed attempt restores what it moved.
ποΈ The reorganization (#522)
104 engine modules moved into
py_router/, 11 leaf tools intopy_tools/, and the plugin resolves its engine directory itself so PCM installs are unaffected. Beyond tidiness, the point is that the engine is now a thing with a boundary β you can tell at a glance whether a file is the router or a tool that uses it, and the parity gate that guards CLI/GUI drift can name the engine as a path prefix instead of a hand-kept list.We took the layout seriously enough to verify it behaviorally rather than by imports: replaying a recorded chain on the old and new layouts produced copper-identical boards, geometry hash included. Three real bugs surfaced because of that comparison and were fixed before it landed.
π¬ The honesty section β treat this as a beta
This is a beta. Not the code quality β the ordering. A new chain sequence is the single most invasive thing we can change, it lands in both the CLI and the GUI at once, and it is new. Commands moved (
py_router/), a whole step disappeared, flags that used to exist are gone rather than deprecated, and the plane work now happens somewhere it never used to.So: keep your 0.19.3 chain scripts until you've run the new one on a board you know well, put the plane nets in your route step's
--nets, and grade the result (check_drc.py+check_connected.py) rather than trusting the summary. If something looks wrong, tell us β that's exactly the feedback this release needs.Full commit-level detail in the v0.19.3β¦v0.20.0 compare (144 commits) and the Rust changelog in
rust_router/README.md.Bug reports and feedback hugely welcome β open an issue or reply here. Happy routing! π
All reactions