Architecture

GDS file ──▶ load_gds ──▶ flatten (apply transforms) ──▶ FlatLayout
                                                              │
                        compute *eager* virtual layers ◀──────┤
                                                              ▼
PDK + deck (YAML) ──▶ rules ──▶ run each check ──▶ Violations ──▶ .lyrdb
                                    │
                                    ▼
                      MergedCache (tiled merge, per-layer halo,
                      lazy virtual layers, region stitching,
                      net extraction) — shared across all rules

Design goals

gdscheck targets real, full-chip layouts, not just small test patterns. That drives two constraints that shape everything below:

  • Memory must stay bounded regardless of chip size. A dense layer like Activ or GatPoly on a full SoC top cell can carry many millions of shapes; no single check may ever union or hold that in memory all at once.

  • A single run pays once for shared geometry. A deck touches the same layer from several rules (e.g. Metal2 from a width, a space and a density rule); the merged geometry for a layer is computed once and reused, not recomputed per rule.

Hierarchy flattening

flatten.rs resolves every StructRef/ArrayRef in the cell tree under the requested top cell by composing affine transforms (translation, rotation, reflection, magnification, and array repetition) down the hierarchy, producing a single FlatLayout — boundaries and text labels indexed by (gds_layer, gds_datatype) in top-cell coordinates. Only the layers actually referenced by the deck (plus any layers feeding a referenced virtual layer, transitively, plus the connectivity graph’s layers if net-aware checks are running) are flattened — an unreferenced layer never enters memory.

The tiled merge cache

MergedCache (merge.rs) is the shared, per-layer merged-geometry cache every geometric check reads from. Instead of unioning a whole layer’s shapes in one operation, it splits the chip into fixed-size tiles (TILE_UM, 20 µm) and merges each tile’s shapes independently — a tile’s output only depends on shapes within that tile plus a halo margin around it, so tiles merge in parallel and none of them ever holds more than a local neighbourhood’s worth of geometry.

A check calls MergedCache::ensure for a layer; the first check to touch a layer pays for its tiled merge, every later check reading the same layer reuses the cached result.

Per-layer halos

A tile’s halo must be at least as large as the biggest distance any rule measures on that layer, or a shape just across the tile boundary could be missed and a real violation (or a real pass) computed wrong at the tile’s edge. The halo is per layer, not per run: before the merge cache is built, lib.rs scans every distance-based rule (min_width, max_width, exact_width, min_space, min_notch, min_enclosure, max_enclosure) and records, per layer, the largest value referencing it. A deck-wide halo would let one coarse rule (e.g. a 1500 µm max_width on a guard layer) inflate the merge of every fine layer in the deck — for a dense layer that difference is the difference between a normal run and one that never finishes.

A min_space/min_notch rule referencing a layer with no shapes at all in the current design is skipped when computing halo (an empty partner must not inflate the other layer’s halo just because a rule exists on paper).

Region stitching

Some questions can’t be answered from one tile in isolation: “is this whole connected region wide enough,” “does this whole region touch a text label anywhere,” “does this whole region interact with another layer.” stitch_regions/stitch_labeled (merge.rs) answer these without ever reconstructing or globally unioning the source geometry: each tile independently computes the piece of every region that falls in its core (as opposed to its halo), and pieces in adjacent tiles are joined with a union-find whenever their shared core edge shows continuous coverage. A region’s aggregate property — total area, a representative marker point, or a caller’s boolean predicate (“does any piece touch layer B”) — is then just an OR/sum reduction over its pieces, which stays cheap however many tiles a real-world region spans.

This is what lets checks like windowed-density’s plate analysis, or the interacting/not_interacting/with_text virtual-layer selectors, run correctly on chip-spanning regions while only ever touching tile-local, halo-bounded geometry.

Lazy virtual layers

A PDK’s virtual_layers: (declared in pdk.yml, see Virtual layer operations) come in two evaluation modes:

  • Eager (the default) — computed once, up front, as ordinary boundaries inserted into the flattened layout (pdk.rscompute_virtual_layers). Fine for small or sparse derived layers.

  • Lazy (mode: lazy) — registered with the MergedCache as a TiledVirtual: a synthetic (layer, datatype) key built per tile, on first ensure, by applying a boolean/selection/morphological op to its source layers’ tiles (each recursively ensured in turn). A lazy virtual layer costs nothing until something actually asks for it, and its memory profile is the same tile+halo bound as any drawn layer — the mode a dense, multi-step derivation (like the antenna forbidden-region chain in forbidden_unless_labeled) must use to stay bounded on a full chip.

A lazy virtual layer that feeds another must have its own halo raised to cover the downstream layer’s needs, transitively — lib.rs propagates this before the merge cache is constructed.

Net extraction

Net-aware checks (the antenna-ratio family, gate-connected minimum area) need to know which shapes are electrically the same net. connectivity.rs extracts nets from geometry alone: the PDK declares a list of connect specs, each a connector layer (a via or contact) and the conductor layers it bridges. A layer’s own connected regions are already available via stitch_labeled; a connector sits inside every layer it joins, so a single point of the connector lands in one region of each — those regions are unioned into one net via a union-find over (layer, region) nodes. Extraction is lazy: it only runs if the deck actually has a net-aware check (see NET_AWARE_CHECKS), so a geometry-only deck never pays for it, and it can be disabled outright with --no-connectivity.

Memory bounding and eviction

Two further mechanisms keep peak memory flat as a deck grows:

  • Eviction. lib.rs records the last rule index that references each layer before running any rule. Right after that rule runs, the layer’s cached tiles (and any stitched regions) are dropped from the MergedCache. A deck touching dozens of layers only keeps a handful resident at any point, not all of them at once.

  • Per-check-run scoping. Only layers actually reachable from the current deck’s rules (transitively through virtual-layer sources and the connectivity graph) are flattened from the GDS in the first place — a layer the deck never references never exists in memory at all.

Parallelism

Every tile-level operation (raw-layer merge, virtual-layer composition, region stitching, per-window density) fans out across rayon’s thread pool, one task per tile key; the --threads flag (default: all logical cores) controls the pool size. Because tiles are independent (correctness depends only on the halo, not on execution order), this parallelism is exact, not an approximation — the result of a threaded run is identical to a single-threaded one.