Generators Overview

track_gen ships six registered centerline generators. Each generator is pluggable through TrackGenConfig.generator and follows the same pipeline contract: it writes one closed centerline per environment, then the shared resample → XPBD relax → inflate → validity stages take over — except repulsive, which is not CUDA-graph-capturable (see below).

Grid of sample tracks produced by each generator

Sample tracks from the six generators (one row per generator).

The Six Generators

bezier

Samples random grid-jittered corners, sorts them by angle around their centroid, and assembles a closed cubic Bézier spline. Handle lengths are proportional to rad * chord and clamped by handle_clamp_frac so handles cannot overshoot nearby corners. Optional style_sampling draws per-environment rad, scale, and handle_clamp_frac from configured ranges, broadening the shape family without changing the representation. Self-crossing smooth curves fall back to their provably simple corner polygon, which XPBD re-rounds. See Bezier Generator for the full deep dive.

hull

Angle-sorts a set of randomly sampled points around their centroid — a static stand-in for a convex hull — then interleaves each adjacent pair of sorted vertices with one radially displaced midpoint. The augmented loop is smoothed by a closed uniform Catmull-Rom spline. hull_displacement controls how strongly midpoints are pushed outward (lobes) or inward (pinches and straights). Catmull-Rom self-crossers fall back to an arc-resampled straight augmented polygon. See Hull Generator for the full deep dive.

polar

Builds polar_num_knots control knots in polar coordinates: each knot starts at an evenly spaced angle, receives bounded angular and radial jitter, and the resulting sequence is smoothed by a closed uniform Catmull-Rom spline. The output is bounding-box normalized to the shared target extent. Because the shape is smooth and centered by construction, polar carries the lowest relaxation burden of the five generators and has no generator-local fallback. See Polar Generator for the full deep dive.

voronoi

Draws a fixed site field under one of four layouts (ring, void_ring, clustered, mixed) and selects voronoi_control_points anchor sites by snapping angular sector targets — jittered by voronoi_angular_jitter with radius modulated by voronoi_radial_variation — to the nearest unused site. The anchor cycle is rounded by one Chaikin subdivision and a closed Catmull-Rom pass, then arc-resampled. Self-crossing smooth loops fall back to the selected anchor polygon. The void_ring default produces the highest compactness of the five generators. See Voronoi Generator for the full deep dive.

checkpoint

Steers a bounded-turn path through checkpoint_count radial checkpoints drawn at uniformly spaced angles with bounded jitter, producing an organic, continuously undulating loop unlike the star-shaped families. A fixed num_points-step walk with heading gain checkpoint_steer_gain and per-step turn clamp checkpoint_turn_rate replaces CarRacing’s unbounded retry loop. An additive heading-ramp closure ensures the net turning number is exactly one (no inner loops) while preserving the steered local curvature. Best-of-K selection (default K=4) keeps the least-self-intersecting of K decorrelated candidates per environment; an optional single-crossing clip fallback is available but off by default. See Checkpoint Generator for the full deep dive.

repulsive

Grows each centerline from a small seed circle under a hard ratcheting length constraint, using a tangent-point energy and a Sobolev-preconditioned flow to keep the curve self-avoiding while it is confined to a disc domain seeded with random obstacles — coarse-to-fine (N = 64 → 128 → 256) with an area-based stall-stop. It is the only generator built from iterative physical simulation rather than a single deterministic sampling + smoothing pass, and produces the foldiest, most serpentine circuits of the six (compactness ≈ 0.15). It is also the first generator that is not CUDA-graph-capturable (capturable=False): its host-side stage transitions and stall-stop readback are illegal inside a capture region, so it runs eagerly on CUDA every call — roughly 1000× slower than bezier (~0.2 s @ E=64, ~5 s @ E=8192 on an RTX 4090). Prefer a slow regeneration cadence or staggered per-env slices over calling generate() every step. See Repulsive Generator for the full deep dive.

When to Use Which

Generator

Shape character

Typical compactness

Best suited for

bezier

Star-shaped corner families; smooth cubic curves

Low (~0.44)

General-purpose diversity; most configurable style knobs

hull

Angle-sorted loop with radial lobes and pinches

Low (~0.43)

Tracks with pronounced lobes, pinches, or straight sections

polar

Smooth periodic radial spline; centered by construction

Medium (~0.56)

Low-burden contrast to corner families; fast, symmetric shapes

voronoi

Graph-cycle anchored to a site field; compact and feature-rich

High (~0.73)

Complex, compact layouts; largest lap-time variety

checkpoint

Flowing organic loop from bounded-turn steering

Medium-high (~0.61)

Organic circuit feel; CarRacing-style continuously curving tracks

repulsive

Dense, foldy serpentine from self-repulsive growth

Very high (~0.15)

Maze-like circuits; only when the ~1000× generation cost (non-graph-captured, see Repulsive Generator) fits your regeneration cadence

Generator Contract

A generator produces the initial closed centerline fed to the shared pipeline. It implements two callables: alloc_scratch(config) allocates fixed-shape, generator-private Warp buffers once at construction time, and generate(seeds_wp, config, out_centerline, out_valid_wp, scratch) writes one closed centerline per environment into the orchestrator-owned output buffer in place. The hot path must be pure Warp and deterministic in (seed, config). For the five capturable=True generators it must additionally be zero-allocation inside generate and CUDA-graph-capturable — no host-side retry loop, no host branch on generated tensor data, and no per-environment Python branching inside generate. A generator may instead declare capturable=False (currently only repulsive) to use host-side control flow and per-call allocation; it then runs eagerly on CUDA every call instead of being captured into a replayable graph. Generators set out_valid_wp to 1 for every environment at this stage; final geometric validity (turning number, thickness, NaN checks) is decided later by the shared post-relax inflation validity gate. The full contract, hard rules, and registration instructions are documented in Writing a Generator.

Benchmarks

The generation cost of each centerline generator is summarized below; a companion shape/geometry-metrics comparison table will follow later (the full quality/diversity metric suite already lives in Generator Benchmarks). All rows were measured on an NVIDIA RTX 5000 Ada at num_envs = 4096 with the library-default TrackGenConfig (half_width = 0.1, num_points = 256, relax_iters = 50), timing the full TrackGenerator.generate() call — centerline generation plus the shared resample → XPBD relax → inflate stages — since only the centerline generator differs across rows. warmed ms/call is the steady-state mean over 10 replays after a warmup call; for the five capturable generators this is a CUDA-graph replay. first call (s) is that warmup call, which pays CUDA-graph capture plus cached Warp module load. Each row was measured twice and agreed within ~10%.

Generator

warmed ms/call

first call (s)

yield

bezier

10.1

0.05

0.998

hull

11.1

0.05

0.992

polar

7.7

0.04

1.000

voronoi

13.9

0.06

1.000

checkpoint

13.7

0.06

0.995

repulsive [1]

~1880

~1.9

0.997

The first call (s) figures above are taken with Warp’s on-disk kernel cache warm (capture plus cached module load only); the very first run after a fresh install or a kernel-source change additionally pays a one-time ~4 s Warp JIT compilation that is shared across all generators.

To reproduce per-generator timing at this scale, use benchmarks/compare_generators.py (its gen_ms_per_call column is the warmed steady-state generation time):

.venv/bin/python -m benchmarks.compare_generators --cuda --E 4096