Contents

ADR-0002: Spec-first pipeline and what "deterministic" means

  • Status: Accepted (2026-09-29)
  • Date: 2026-09-29

Context

The brief makes this non-negotiable: AI authors structured, versioned, schema-validated specs; deterministic systems build assets and perform episodes; same specs and seeds give the same assets and frames. "Same frames" needs a precise definition, because GPUs, drivers and renderers differ. Measured on Godot 4.7.2: repeated runs on the same machine and renderer are bit-identical, but a flat 0.3 grey clears to 77 on Metal and 76 on Mesa lavapipe.

Decision

  1. Decide upstream, draw downstream. Every choice, random or AI, is made in the core and recorded in a spec. The engine is a pure function of its resolved inputs.
  2. Four levels of determinism, each with its own test:
    • L1 specs: byte-identical on every platform.
    • L2 geometry: identical within float tolerance on every platform.
    • L3 frames: bit-identical for one engine version, renderer and driver. Across renderers they are perceptually identical (measured Metal vs lavapipe on the demo: mean difference < 0.06 levels, with a few antialiased edge pixels off by up to 64).
    • L4 files: byte-identical deliverables for one ffmpeg build.
  3. Seeds by name path, never by draw order. seed(child) = first 4 bytes, little-endian, of SHA-256(le32(parent) ‖ UTF-8(path)). Samplers (core only) draw from PCG32 streams seeded this way. deriveSeed is also implemented in GDScript and checked against shared test vectors.
  4. Stateless randomness in the engine. Per-frame noise (line boil, grain, flicker, starfields) is pcg_hash over (seed, frame, index): a 32-bit integer hash that fits int64 GDScript math and shader uint math alike. The engine never keeps a sequential RNG.
  5. Playback is evaluate(frame). No state accumulates across frames. Any stateful simulation is replayed from the start of its shot.
  6. No hidden inputs. No wall clock, no global RNG, no randomize(). A CI grep enforces this. One exception, outside every render: ui/rw_wall_clock.gd gives Studio's interactive players and fades the real seconds between frames, and the grep fails if anything under runtime/ or cli/ names it (2026-10-10).
  7. Tier 2 outputs are frozen. Generative providers are not reproducible, so their outputs are stored by content hash and referenced by hash. Specs plus frozen assets are reproducible.

Alternatives considered

  • Bit-identical frames everywhere. Not achievable across GPU vendors and drivers without a pure-CPU software renderer for every render, which is far too slow for 1080p or 4K.
  • Order-based RNG streams (one seed per document). Simpler, but adding one attribute to a pattern would reshuffle every attribute sampled after it, breaking "change one thing".

Consequences

  • Golden frames are kept per renderer and enforced exactly on CI's canonical renderer (lavapipe). Comparisons on other renderers are informational: expect a mean difference under 0.1 levels, with a few edge pixels off by more.
  • Seekable playback costs some upfront design (no free-running particles or physics) and buys single-frame previews, scrubbing, chunked renders and meaningful golden tests.