Rust/WASM core, WebGL2, zero dependencies

The whole chip,
in a browser tab.

Manhattan is a chip-layout viewer for designs with hundreds of millions of placements. It streams a tile pyramid to a WebGL2 renderer written in Rust, so you can go from the full die to the pins of a single cell without ever loading the design, then share exactly what you see as a link.

The Manhattan viewer showing the routed sky130 gcd design: rows of standard cells in blue, metal2 routing in orange, metal3 in purple, with the layer panel open on the right.
Captured from the real viewer: OpenROAD's routed sky130 gcd, with real placement and real routing on metal1-3.
349Mplacements at the chip view, drawn from one tile fetch
2.4 sfrom an 80 MB real LEF/DEF to a viewable chip
8draw calls per block, whether the cell library has 390 masters or 4,634
20 KBgzipped Rust core. No framework, 0 npm and 0 cargo dependencies

The problem

A chip doesn’t fit on a GPU. It doesn’t fit in a tab either.

A modern block has tens of millions of cell placements, and a chip places that block dozens of times. Flattened, the synthetic test chip here is 8.2 GB of placements. The layout tools engineers use today load the design into memory. One was observed sitting at Mem: 9.0GB.

Manhattan never loads the design. It keeps a 64 MB tile cache, draws only what is on screen, and draws it at the level of detail the screen can actually show.

One zoom, four representations

From 349 million placements to a single cell.

The data is a tile pyramid, and it doesn’t decimate as you zoom out. It changes representation, and the viewer picks the level from your zoom and a rectangle budget. Every image below is the real viewer rendering a synthetic 5M-placement block placed 70 times.

The full synthetic chip: a 9 by 8 grid of 70 identical blocks, each shown as a density map with macros. One block at far zoom: a density heat map with purple macros and a red power grid. Mid zoom: thousands of individual cell outlines in rows, with a macro and power straps. Deep zoom: individual cells with diffusion, poly, contacts and pins, between power rails and straps.
1 tile fetched, 70 draws The block is tiled once and the chip is a list of 70 transforms over it: 349,349,770 placements in the block’s 117 MB, instead of about 8.2 GB flattened. The whole frame is 73,500 rectangles.
Merged density, macros and the power grid Placements merge into density blocks, coloured on a ramp stretched over the design’s own 5th to 95th percentile. Filler and decap area are shown as dead space.
One rectangle per placement, one draw call Each cell becomes its outline. The level is dropped at 1.84 px per cell, before it turns into noise, and a 1.3× hysteresis band stops it flickering on the boundary.
Every rectangle of every master Cell geometry comes from textures, orientation is applied in the vertex shader, and layer order comes from depth instead of a sort. Draws are grouped by rect-count bucket, so the library size doesn’t change the draw count.

Architecture

Tiled once, offline. Read in place, never parsed.

Tiles are binary, laid out so both JavaScript and Rust take views straight over the fetched bytes. Everything expensive happens once, in a process, and the browser only moves bytes and draws.

  1. 01 Import or generate

    LEF/DEF importer or synthetic generator. Both produce one design object in integer nanometres.

    LEF · DEF
  2. 02 Pyramid writer

    Three tile kinds, draw buckets solved by dynamic programming, and a LOD ladder solved from the data.

    tiles/{z}/{x}/{y}.bin
  3. 03 Tile server

    Serves written tiles, or builds deep tiles on request from a 4-byte-per-placement index in 0.22 ms (p50).

    placements.bin
  4. 04 Fetch queue

    Ranks requests by distance from the camera, aborts the ones you’ve panned past, and evicts least-recently-used tiles from a 64 MB cache.

    8 in flight
  5. 05 Rust/WASM core

    Residency, pooled GPU buffers, the instance table, LOD and picking. It calls WebGL2 directly.

    49,841 B · no_std
  6. 06 WebGL2

    Instanced draws, with all 8 LEF orientations and layer depth handled in the shader.

    ≤ 8 draws / block

Engineering

Costs that don’t grow with the design.

Every number here was measured in the repository. Where a figure is extrapolated, the docs say so.

4,400 → 8

Draw calls don’t scale with the library

Grouping by master would need about 4,400 draw calls for a 4,634-master library. Grouping by rect-count bucket, with bucket sizes solved exactly from the design’s own histogram, needs 8. On real data that wastes 0.2% of GPU slots, against 45.2% for fixed buckets.

8.2 GB → 117 MB

Instancing, one level up

Masters are instanced into blocks and blocks into the chip, with exact integer transforms. A tile shared by 70 instances is one key, one fetch and one GPU allocation.

0 buffers

A pan rebuilds nothing

GPU slot records don’t depend on the view origin or the instance. A pan changes one uniform. Positions are computed in f64 on the CPU and made relative before they become f32, so zoom stays precise across a 1.6 mm die.

1,185 → 233 MB

Deep tiles, only when asked for

Deep levels are 96% of the pyramid, and a session opens a few hundred of their tiles. Built on request, a tile arrives in 15.56 ms over HTTP, against 15.55 ms for the same tile read from disk. Generating the 50M design drops from 62 s to 15 s.

77%

A frame budget that holds

The LOD switch points are solved per design and per canvas size, not hard-coded. The worst frame anywhere in the zoom range is 1,531,094 rectangles, inside the renderer’s budget of about 2 million.

108 → 13

Fetches that know you’ve moved on

Across 12 rapid camera jumps, stale requests are dropped or aborted: 13 fetches instead of 108. The visible set is pinned before it is requested, which closed a bug where tiles were evicted before they could be drawn.

JavaScript → C → Rust

Ported twice. Byte-identical both times.

The renderer moved from JavaScript to C, then from C to Rust. Each port was accepted only when it regenerated every output file identically: 4,072 of 4,072 files, including the manifest that stores the LOD ladder at full float precision.

The generator loads the same WASM module the viewer does. The switch points written at build time and the ones used in the browser come from the same compiled code.

module49,841 B 19.9 KB gzipped
crate#![no_std], 0 dependencies
GL callsall in Rust, WebGL2 driven directly
heapone heap shared with JS, so pointers mean the same thing on both sides
exports104 entry points
runtime gate178 headless samples across 6 levels, passing

Real designs

Tested on files nobody here wrote.

A format written to fit its own generator will always look fine on that generator. So Manhattan reads real LEF/DEF, and the problems each design exposed are written down next to the results.

The superblue16 die: an orange density map of 680,869 cells around dozens of purple macro blocks.

ISPD 2015 · mgc_superblue16_a

components
680,869
LEF/DEF in
80.3 MB
to a viewable pyramid
2.4 s
on disk
15.9 MB

The library, die, rows and 419 fixed macros are real. This is a placement-contest input, so the cells arrive unplaced and are filled into the design’s own rows.

Close-up of sky130 standard cells in blue, crossed by orange metal2 and purple metal3 routing with vias.

OpenROAD sky130 · routed gcd

placed components
8,171
routed nets
411
routing rectangles
15,424 in 419 shapes
format changes needed
none

Real placement and real routing. Wires, vias and patches pass through the same writer, and the pyramid verifies and passes the runtime gate.

What real data broke

  • 17 real layers, 12 layer ids. 34% of superblue16’s library rectangles had no id of their own and were folded onto metal3.
  • A power grid is 99.7% vias. Its 544,478 rectangles come from only 258 distinct shapes. Signal routing barely repeats at all.
  • Cells extend past their own boxes. 38% of sky130’s rectangles hang outside the cell’s SIZE box, because abutted cells share their power rails.
  • The gate assumed a square die. Synthetic data could never catch that. superblue16 failed it on the first run.

Verification

Identical, or it doesn’t ship.

make verify

Reads every binary back and checks every invariant of the format, including re-solving the LOD ladder and matching it bit for bit.

make check

Runs the real viewer in headless Chrome, with no Puppeteer, and visits every point where geometry provably exists, at every level. An empty frame fails the build.

byte identity

A tile built on request must match the written tile exactly: 4,068 of 4,068 tiles. Both ports were held to the same rule.

A 200 nm bug that every other check passed. Lazy tiles below the first row were 200 nm out. That is smaller than a pixel, so verify and check both passed. Only the byte comparison caught it.
drew 46 of 70 culled-in block instances is what the gate reports when a subtle slot-table overflow is put back in. Without the gate’s second phase, the broken code passed.

Shareable views

The URL is the view.

Paste a link into a design review and the other person lands on the same rectangle, with the same layers and the same selection. The URL updates as you move around, but it doesn’t fill your back button with every pan.

…/src/?data=data-sb16&view=812400,640250,0.018&mask=0x1f4&pick=812533,640190
  • data which design on the server
  • view camera centre in nm, and pixels per nm
  • mask which layers are visible
  • pick the selected placement, so “come look at this” means this

Roadmap

Where it’s going.

  1. done
    End-to-end pipeline

    Generator, three-representation pyramid, lazy tiles, instancing, layer panel, click to identify, shareable URLs, and both gates.

  2. done
    Real LEF/DEF, including routing

    superblue16 and the routed sky130 gcd, imported from the command line or by dropping files into the browser.

  3. done
    Rust/WASM core

    The whole renderer, camera, LOD and picking, byte-identical to the C it replaced.

  4. next
    A wider layer id space

    Room for a real 17-layer stack, so a macro shows its pins on metal5 instead of rendering as one solid slab.

  5. next
    Routing as wires, not placements

    A fourth tile kind and a net table, so power grids and signal routes are stored as what they are.

  6. later
    Names and search

    Master and instance names as side tables, so you can search for a cell and jump to it.

Source access

The code is private. The door isn’t.

About 14,000 lines of Rust and JavaScript, the full commit history, and the docs behind every number on this page. If you’re reviewing this work for hiring or a collaboration, ask and I’ll give you read access.