Rules & Invariants

RULES.md in the repo root is the source of truth. If this page disagrees with that file, that file wins. This page adds context about where each rule is enforced, what files are involved, and what actually goes wrong when you break one.

Rule 1 — Simulation is Authoritative

Render gets a RenderSnapshot, audio gets a lock-free command ring, UI gets a UiSnapshot, and none of them write back into World mid-tick. UI requests go through events and are picked up on the next tick boundary during sim stages.

Rule 2 — Fixed-Step Ticks

dt comes from HostConfig::tick_rate_hz at startup and never changes mid-session. Real time feeds TickClock; excess ticks beyond max_catch_up_ticks are dropped and counted. Variable dt breaks replay and destabilizes physics.

Rule 3 — Determinism

Same machine, same build, same inputs, same seed should produce identical output. RNG must go through SimRng. Event and command merge order is deterministic. Map iteration order is banned as behavioral input; collect and sort first if you need stable ordering.

Rule 4 — No Blocking in Sim

No synchronous file reads, no sleeping, no unbounded mutex waits. File IO, cooking, sqlite, and log flushing live on the IO thread pool and re-enter the world through tick_io_boundary().

Rule 5 — Command-Buffered Structural Mutation

Spawns, despawns, and archetype moves cannot happen inside a running stage. Structural mutation goes through EcsCommandBuffer and is applied at stage boundaries so chunk iteration never sees stale structure.

Rule 6 — Declared Reads/Writes

Systems must declare component access via reads() and writes(). The scheduler uses those declarations to build edges. Undeclared access can create invisible conflicts; debug builds catch this with AccessValidator.

Rule 7 — Order is Explicit

If system A must be visible to system B, that dependency needs to be declared through access conflict or explicit ordering. Deterministic topo sort is not a substitute for an actual dependency.

Rule 8 — Events are Bounded

Events<T> enforces max producers and max events per producer. Overflow drops oldest and increments counters. Mid-tick background emissions do not land in the current visible set.

Rule 9 — Background Results Enter at Boundaries Only

IO completions and file-change notifications go into bounded queues and are drained by tick_io_boundary() before sim.tick(). That keeps visibility aligned with deterministic boundaries.

Rule 10 — GPU Handles Stay Out of Sim

Sim stores AssetId, not GPU objects. GPU resources are created and cached in the renderer. Cache miss behavior is explicit through fail-fast, warn-and-placeholder, or silent policies.

Rule 11 — Instrumentation

Every subsystem needs stats resources, drop counters, overflow counters, error counters, and timing on hot-path operations. Crash context must contain build ID, tick number, stage, RNG seeds, and recent logs without blocking the sim thread.

Rule 12 — Scope Boundary

No physics, networking, or animation in engine crates. Those go in modules when a game needs them. Core crates should stay small and stable.

Rule 13 — No Hot-Path Heap Allocation

Per-tick heap allocation on the sim thread is a bug. EcsCommandBuffer, EventBus, and scheduler execution are designed around bounded or warmed-up steady-state paths. Vec::collect() in a hot system is a bug unless explicitly documented.

Rule 14 — Bounded Means Enforced

"Bounded in theory" does not count. Every bounded queue has to enforce the bound at runtime and increment counters on overflow.

BoundConfig fieldOverflow behavior
Max ticks per frameHostConfig::max_catch_up_ticksdrop excess, count timing_drops
Max events per producerper-bus fielddrop oldest
Max producersper-bus fielderror on registration
Max log entries per bufferring capacity from IoConfigoverwrite oldest
Max input events per frameInputConfig::max_input_events_per_framedrop oldest

validate() on config structs must reject out-of-range values at startup, not on a hot path.

Rule 15 — Producer/Buffer Ownership

ProducerBuffer is !Sync. One thread, one producer. Multi-thread emission works by giving each thread its own bus-issued ProducerId. Those IDs must never be fabricated manually because density and session stability are part of deterministic merge behavior.

Other Policies

  • Coordinates. Right-handed, Z-up, forward is -Y, meters, radians, and clip depth 0..1 under wgpu convention.
  • Tier linkage. Tier 1 does not import Tier 2. Tier 2 inter-module dependencies are tightly constrained and should remain mostly flat.
  • Audio overflow. Bounded ring behavior is deterministic and prefers dropping low-priority coalescable commands first.
  • Error handling. Hot paths use compact typed error codes with no heap allocation; panics are for invariant violations only.
  • Slint and wgpu pinning. Slint stays isolated to UI crates, and the repo must not drift into multiple wgpu versions.