Skip to content

The horizon: does a world still look like itself after minutes?

Everything else on this page measures the first seconds of a preset’s life. The four behavioral gates capture 30 frames — half a second — and the reactivity gate a few seconds of hops. A live set runs for hours, and a world whose mechanism accumulates can drift across that gap with the whole suite green: Plan 0075 cohort 4’s Shatter piled onto its flow field’s attractors over minutes and collapsed live three times without a single test going red.

--horizon is the instrument for that class. It renders N simulated minutes at the fixed 1/60 s capture step and prints one row per interval — coverage, concentration (peak/mean), and motion over the figure’s own footprint since the previous row — plus a trend line per statistic.

Terminal window
# Ten simulated minutes of a swarm world, a row every 30 s
cargo run -p standalone --example shot -- \
--preset-file presets/swarm_shatter.toml --horizon 10 --size 96x96 --set bass=0.7
# ...and the same run as JSON
cargo run -p standalone --example shot -- \
--preset-file presets/swarm_shatter.toml --horizon 10 --interval 60 --json

When to run it (ADR-0099 states the trigger once, so it can be found): on a world whose mechanism has an accumulation axis — sustained forces, feedback with net gain, a population that can migrate or pile up. Record the verdict in the world’s own header, the way the fold-edge verdicts were recorded.

Five things to know before reading a row.

  1. It is never a gate. Nothing here runs in CI or fails a build, and the mode applies no threshold. It prints a trend; the verdict is yours. Making it a gate was rejected on measured cost — the reactivity gate’s move to real PCM alone cost 1.8x over 41 presets, and a minutes-long capture per preset is orders beyond that.

  2. Run a static control beside the subject. The statistics are image-domain proxies for a simulation-domain event: particles piling onto attractors reads as coverage falling and concentration rising, and that correlation is strong without being identity. A world that renders one frame forever prints delta 0.0000, monotone 0.00 on every statistic — that flat series is what makes a sloped one mean something.

  3. It is slow by construction — 3,600 renders per simulated minute. At 96x96 on a hardware adapter a ten-minute horizon measured 16 s for a single-pass world (Halo) and 54 s for a reaction-diffusion one (Etching, 13 passes a frame). Nobody will run it casually, which is the honest cost of refusing a gate.

  4. The verified length is the documented one, and here is what verified means (Plan 0099). --horizon 10 renders all 36,001 frames on all three shipped reaction-diffusion worlds — Etching, Mitosis, Verdigris — with the resident set flat: 324 MB reported before the run and 400 MB after, of which the render itself travels 0.8 MB (398.8 MB at 4 s to 399.6 MB at 54 s) — the 21 sampled images themselves. Measured on the Windows development box, hardware adapter, debug build, at 96x96 (ADR-0071 — a different machine or profile is a different measurement).

    It was not always true, and the shape of the old failure is worth keeping. The capture path submitted every non-sampled frame without ever polling the device, so it reclaimed nothing between two sampled frames — 1,800 consecutive unpolled submits at the default interval. Retention was per pass, not per pixel: an RD world retained 950 KB a frame against a 36 KB captured frame, where single-pass worlds retained ~30 KB, so RD reached the allocator first at ~4.4 GB and died with an invalid readback buffer. That made it look like a property of the RD family; it was the poll cadence. The step now polls, which costs about 1.5x wall clock on an RD world and bounds the memory of any run at any length.

    A run that cannot reach its requested length says so on stdout, in the table’s place, with the wall clock and resident set it died at — not only as a stderr line. And a --horizon the --interval does not divide is rounded down to the last whole interval (rows are exact multiples, which is what makes them comparable between runs), so the header states the length the run actually reached and flags the shortfall when there is one.

  5. It cannot see most of the process. GPU resource churn and the frame-time spike on a preset switch are not reproducible in a headless loop that never rebuilds a surface. --soak is the instrument for that half, and the two are deliberately separate (ADR-0099).

    One exception, and it is the one that mattered: the resident set of the render loop itself. The cost block reports it, and reading that column is what found the ceiling in point 4 — a headless loop cannot reproduce a switching app’s memory behaviour, but its own growth is exactly what a long run is in a position to see. This point used to name the resident set among the things a horizon cannot see; that was too broad.

The stimulus is --set, held for the whole run, which is what makes a row at minute nine comparable with a row at minute one — so --horizon and --signal/--audio are mutually exclusive rather than one silently winning.

Reading the trend block: delta is how far a statistic travelled end to end, and monotone is what share of the steps went that way. A world grinding into a corner reads a large delta at a monotone near 1.00; a world breathing around a stable mean reads a delta near zero whatever its monotone. Where the line falls between drifting and alive is a judgement about the look, which is why the tool declines to make it.

Two properties hold and are asserted rather than assumed (standalone/tests/shot_cli.rs): the same world at the same horizon produces identical rows across runs, and a row at interval k does not depend on how far the run was asked to go — so a two-minute run and a ten-minute run agree on every row they share. The wall clock and resident set in the cost block are the exception: those are properties of the box, reported and never asserted (ADR-0071).

Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.