Skip to content

fix(router): enforce via hole-to-hole spacing at probe time#672

Merged
ecto merged 6 commits into
mainfrom
fix/router-via-hole-to-hole
Jul 25, 2026
Merged

fix(router): enforce via hole-to-hole spacing at probe time#672
ecto merged 6 commits into
mainfrom
fix/router-via-hole-to-hole

Conversation

@ecto

@ecto ecto commented Jul 25, 2026

Copy link
Copy Markdown
Owner

The hole

RouteSession::probe filters candidates by layer (e.layer != layer) because copper is indexed per layer. Two vias whose layer spans do not overlap share no copper element, so the oracle never compared them — while their drill barrels, which are layer-independent, could physically collide. Every clearance check passed; the board was unmanufacturable.

Evidence from the 2026-07-24 CM5 fab campaign: a routed board carried 9 cross-net via pairs closer than the hole-to-hole rule, worst -0.024mm overlap (/MIPI0.D3_N vs /USB3-0.DM). They were caught only by a post-hoc python scan and fixed by strip-and-reroute. router::legalize does have a fail-closed HoleToHole demote path, but it fixes one violation per round under MAX_FIX_ROUNDS = 4 — nine of them outran it.

The fix

A second index in the session, over drilled holes rather than copper:

  • probe_hole / probe_via_hole — mirrors drc::check_hole_to_hole exactly: illegal when dist(centers) - rA - rB < rules.hole_to_hole - 1e-6, regardless of layer span. Seeded from pcb.vias (any span) and through-hole pad drills.
  • commit_hole — hole ids share the SpanId space, so remove rips a via's barrel out with its copper and rip-up-and-reroute keeps working. Separate tombstone counter and compaction per index.
  • via_drill_for — per-net-class drill diameter, mirroring legalize::via_geom_for, i.e. how the router sizes a via on write-back. No drill plumbing needed at call sites.

Wired into every via-placement path:

site what it gates
maze::via_ok in-search, so A* avoids bad via sites instead of failing at commit
auto::escape_endpoint::via_legal escape / dog-bone / plane-stitch vias
auto::validate_and_commit the commit gate — last line of defense
complete::edge_ok the complete window router's via edges
auto::commit_via now also commits the barrel, so the index stays complete

Two deliberate semantic choices, both documented in the code:

  • Same-net holes are exempt. Coincident same-net drills are the merge case, already owned by legalize::merge_same_net_vias — refusing them at probe time would break at-pad stitching.
  • Net ties are NOT exempt. A tie is an electrical exemption; a drill is mechanical. This matches the DRC pass exactly, which is the point.

Pad drills are included, not just vias. The task's evidence was via-vs-via, but it's the same rule and the DRC already flags via-vs-pad — excluding pads would leave router and DRC disagreeing, which is the failure mode being fixed.

Verification

  • New router test via_refused_when_drills_collide_across_disjoint_layer_spans: a blind FCu..In1Cu via and a buried In2Cu..BCu via 0.5mm apart on a 4-layer board. Asserts every copper probe passes (that is the hole), the hole probe reports the 0.1mm gap and names the blocker, validate_and_commit refuses and session.len() is unchanged, a 1.0mm placement is accepted, and check_drc flags the same pair as HoleToHole — so the session and the DRC agree.
  • Two session tests: rule arithmetic + boundary + same-net exemption + rip-up reopening the spot, and via-vs-through-hole-pad.
  • cargo test -p vcad-ecad-pcb --features gpu — 255 passed, 0 failed.
  • cargo clippy -p vcad-ecad-pcb --features gpu -- -D warnings — clean. (--all-targets has 2 pre-existing items-after-test-module errors, present on main at 1e69b5a.)
  • 40-net CM5 subset, built --release --features gpu: output is identical to the pre-fix baseline — 1292 segments, 84 vias, 39 routed / 1 unrouted (the same pre-existing plane-stitch failure on Net-(C240-Pad2)) — with zero cross-net via pairs under the rule either way. No routability or wall-clock regression; the hole query is one small R-tree lookup per candidate via.
  • Full CM5 route is running now (~2h). I'll post the cross-net via-pair scan of /tmp/out.pcb.json on this PR when it lands.

Reviewer note: the diff-pair test board changed

router::pair's pair_board declared hole_to_hole: 0.5 with via_drill: 0.2 on legs gap + width = 0.45mm apart. Two 0.2mm drills 0.45mm apart leave a 0.25mm gap — under that rule no pair layer transition on that board is manufacturable, and the two transition repros only passed because the oracle couldn't see it. The board now declares the 0.2mm rule its own geometry implies, and both repros assert assert_twin_holes_clear so the invariant is checked rather than assumed. CM5 itself uses holeToHole: 0.25, so real pair transitions are unaffected.

🤖 Generated with Claude Code

The session's legality oracle only compared copper, and copper is
indexed per layer. Two vias whose layer spans do not overlap share no
copper element, so `RouteSession::probe` never compared them — while
their DRILL barrels, which are layer-independent, could physically
collide. The 2026-07-24 CM5 fab campaign found 9 such cross-net pairs
on a routed board (worst: -0.024mm overlap, /MIPI0.D3_N vs
/USB3-0.DM); they were caught only by a post-hoc scan.

Give the session a second index over drilled holes — via barrels and
through-hole pad drills — with `probe_hole` / `commit_hole`, mirroring
`drc::check_hole_to_hole` exactly: illegal when
`dist(centers) - rA - rB < rules.hole_to_hole - 1e-6`, regardless of
layer span. Hole ids share the span-id space, so rip-up removes a via's
barrel with its copper. Same-net holes stay exempt (coincident same-net
drills are the post-route merge case); net ties are not exempt, since a
tie is electrical and a drill is mechanical — exactly as the DRC treats
them.

Wired into every via-placement path: the maze's in-search `via_ok`, the
escape/stitch `via_legal`, `validate_and_commit`'s commit gate, and the
complete window router's via edges. The hole query is one small R-tree
lookup per candidate via; a 40-net CM5 subset routes byte-identically
to before at the same wall-clock.

The diff-pair repro board's rules were internally inconsistent — a
0.5mm hole-to-hole rule with 0.2mm drills on legs 0.45mm apart makes
every pair transition unmanufacturable — so it now declares the 0.2mm
rule its own geometry implies, and both transition repros assert the
twins' drills keep it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chojiai

chojiai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Choji has used up today's reviews

Your team has reached the free plan's limit of 5 reviews per day, so Choji stepped aside on this pull request. That's a plan limit, not a reflection of the change itself.

Your free reviews reset tomorrow. To have Choji review every push without waiting, upgrade to Pro for unlimited reviews.

You're on the free plan · 5 reviews/day.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
mecheval Ignored Ignored Jul 25, 2026 1:33pm
vcad Ignored Ignored Jul 25, 2026 1:33pm
vcad-docs Ignored Ignored Jul 25, 2026 1:33pm
vcad-mcp Ignored Ignored Jul 25, 2026 1:33pm

Request Review

@ecto

ecto commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Acceptance criterion 2: full CM5 route — zero cross-net via pairs

Fresh full-board route on this branch, no post-pass:

cargo build --release -p vcad-ecad-pcb --features gpu --example cm5_bench
RUST_LOG=warn VCAD_GPU_BATCH=1 VCAD_GPU_NEGOTIATE=1 \
  target/release/examples/cm5_bench .scratch/CM5RevEng.kicad_pcb 1 999999 /tmp/holefix_out.pcb.json
board: 10 copper layers, 436 nets, 479 footprints, 3037 pads
human: 8115 segments, 2902 vias, 7667 mm copper, 409 routed nets
vcad:  7328 segments, 741 vias, 6052 mm copper, 396 routed / 12 unrouted nets
score: routability 0.987, via ratio 0.26x human, length ratio 0.79x human, 6807.4s

Scan over the vias array (position.x/y, drill, net), same arithmetic as drc::check_hole_to_hole:

/tmp/holefix_out.pcb.json: 741 vias, holeToHole = 0.25mm
cross-net via pairs under the rule: 0

0 violations across 741 vias. The 2026-07-24 fab campaign found 9 such pairs on a comparable route — worst -0.024mm (/MIPI0.D3_N vs /USB3-0.DM) — caught only by a post-hoc python scan and fixed by strip-and-reroute. They are now refused at probe time.

Routability 0.987 is at or above the 0.985 recorded best (#669), so the extra constraint did not cost coverage. The 40-net iteration subset also routes byte-identically to the pre-fix baseline (1292 segments, 84 vias, same single pre-existing plane-stitch failure), which is the wall-clock evidence: the hole query is one small R-tree lookup per candidate via.

Also merged current main in — the three unrelated phyz breaks (#673, #675) that were blocking every job before this branch's code could even compile.

🤖 Posted by Claude Code

main gained its own drill index in #677 (`DrillElement` / `probe_drill` /
`commit_drill`), so this branch's parallel `SessionHole` / `probe_hole` /
`commit_hole` is dropped and its wiring re-expressed on main's API. What
survives is the part #677 left unfinished:

- `commit_drill` now returns a `SpanId` and the barrel is tombstoned like
  any other span, so rip-up-and-reroute takes the hole out with the
  copper. Without this the router walls itself in — every via it tried
  and abandoned would keep its ground forever. `probe_drill` skips dead
  barrels; the index compacts on the same half-tombstone rule as copper.
- The check is wired into every via-placement path, not just the complete
  window router: the maze's in-search `via_ok` (so A* avoids bad sites
  instead of failing at commit), the escape/stitch `via_legal`, and
  `validate_and_commit`'s gate. `commit_via` indexes the barrel, which is
  what makes router-vs-router collisions visible at all — on main
  `commit_drill` had no production caller, so only pre-existing board
  holes were ever seen, and the CM5 failure mode (two *router-placed*
  vias) was still open.
- `via_drill_for` / `probe_via_drill` size a candidate from its net class
  the way `legalize::via_geom_for` does on write-back, so no call site
  has to plumb drill diameters.

Adopts main's net-agnostic semantics: the hole rule ignores nets, which
matches `drc::check_hole_to_hole` exactly and is stricter than this
branch's earlier same-net exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ecto
ecto merged commit f851af1 into main Jul 25, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant