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
- 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.
- 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.
- 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.deriveSeedis also implemented in GDScript and checked against shared test vectors. - Stateless randomness in the engine. Per-frame noise (line boil, grain, flicker,
starfields) is
pcg_hashover(seed, frame, index): a 32-bit integer hash that fits int64 GDScript math and shaderuintmath alike. The engine never keeps a sequential RNG. - Playback is
evaluate(frame). No state accumulates across frames. Any stateful simulation is replayed from the start of its shot. - 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.gdgives Studio's interactive players and fades the real seconds between frames, and the grep fails if anything underruntime/orcli/names it (2026-10-10). - 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.