Ranger architecture

5 — The simulation pipeline

Written: 2026-08-15 · Established by: reading src/sim/simulate.ts, src/sim/pool.ts, src/sim/partition.ts, src/sim/field.ts, src/sim/mesher.ts, src/sim/rest-material.ts and src/workers/tri-dexel.worker.ts at commit 93a9a67.

This is where Ranger's time goes, and it is the only subsystem with a hard resolution limit on what it may claim. Ranger models the stock as a tri-dexel field — three perpendicular sets of rays, each ray holding the intervals along it where material remains — and cuts that field by sweeping the tool's swept volume through it.

simulate() in src/sim/simulate.ts is the orchestrator. It never touches a worker directly: it takes a SimulationRunner, which is either the worker pool or the single-threaded inline-runner the tests drive.

Four phases, left to right. The sweep is the long pole and carries the first 90% of the progress bar; the comparison and the mesh share the last tenth.

4 — Mesh, in the workers

runner.mesh, tiled

excessAtVertices

3 — Compare, main thread

assembleField

rasteriseBodies

excessField

clusterRegions

2 — Sweep, in the workers

setProgram

runJob per job

kernel.sweep
4096-segment batches

compact to JobField

1 — Plan, on the main thread

deriveGrid

buildSweepProgram

planJobs

SimulationRequest

SimulationResult

Checkpoint, optional

The two numbers that shape everything above

JOB_BUDGET_BYTES is 24 MB and MIN_JOBS is 64. A job's field costs rays × capacity × 32 bytes whether or not the rays need the capacity, so the budget is what keeps the concurrent peak survivable — eight workers give a 192 MB ceiling on working memory. The floor of 64 jobs exists because jobs are wildly uneven: a slice of the grid the toolpath never visits costs almost nothing once the kernel's region cull has rejected every move, while a slice through the middle of the part carries the whole file. MIN_JOBS is deliberately not a multiple of the pool size, so a scaling measurement compares the same partition run two ways rather than two different partitions.

SWEEP_BATCH_SEGMENTS is 4096, and it is two granularities at once. WebAssembly cannot yield, so the batch boundary is both where progress is reported and the only place a cancellation can take effect.

The comparison happens on the main thread, before the mesh

That ordering is load-bearing. The verdict is computed from the assembled field, not from the mesh, because ADR-0024 states its rules in lattice terms and the mesh is a lossy reconstruction of the lattice. Doing it on the main thread also means the nominal body is rasterised onto the grid this run derived, with no opportunity for a caller to supply a lattice of its own.

Every verdict states the pitch it was computed at, and declines below its own resolution rather than reporting with a caveat. That is the same ADR, and it is the reason rest-material.ts carries a DeclinedRegions type at all.

Resume, and what it is allowed to skip

A run can start from a SimCheckpoint — a snapshot of the field at an operation boundary. longestUnchangedPrefix finds how much of the operation list is untouched, checkpointUsable agrees the prefix matches, and simulate adds one further test the diagram cannot show: reach is a property of the whole program, so a checkpoint is refused if any operation past its own prefix pushed the deepest commanded point lower than the checkpoint assumed.

Where the honest failures are surfaced

overflow counts rays that ran out of interval capacity — the ceiling on how many separate walls one ray may thread. Crossing it is a visible defect, so the count is carried forward across a resume and deliberately double-counts rather than risking an under-report.

MeshTooLargeError carries a suggested coarser pitch instead of quietly decimating the mesh.

Drag to pan · scroll to zoom · Esc to close