Determinism concerns for lockstep/rollback simulation
flecs version: 4.1.3 (C, single-threaded pipeline)
We're building a deterministic lockstep simulation on flecs: multiple machines must produce bit-identical results given the same initial state + the same inputs. A client also restores a serialized world snapshot and re-simulates forward, and that re-simulation must match what the authoritative peer computed. We've hit two flecs behaviors that make this hard, and I'd like to know the recommended way to handle them (or whether they're considered bugs).
Problem 1 — a system that activates mid-frame runs in the same frame, so per-frame results depend on world history
We use a level-of-detail pattern that splits one logical per-entity update into mutually-exclusive systems gated by a tag, all in the same phase:
SysLow : (Position, !FastTag) // handles entities WITHOUT the tag
SysHigh: (Position, FastTag) // handles entities WITH the tag
Both run in EcsOnUpdate (SysLow first), and both do the same kind of per-entity work. The design assumes each entity is handled by exactly one of them per frame.
A separate system in an earlier phase (EcsPreUpdate) adds FastTag to an entity on some frame (like a relevance/interest system re-classifying it). The add is deferred, so it's still pending when the OnUpdate variants run.
Observed: whether the entity is processed once or twice on the transition frame depends only on whether SysHigh was already active (i.e., whether any other entity already had FastTag), because:
- If
SysHigh is dormant (its query matches no non-empty table), adding FastTag mid-progress makes it match → the pipeline is rebuilt → the newly-active SysHigh runs in the same progress, after SysLow already processed the just-tagged entity → double-processed.
- If
SysHigh was already active, the entity is processed once.
So the same per-frame inputs produce a different result depending on world history. In lockstep this desyncs: a client that restored a snapshot in which SysHigh happened to be dormant computes a different result than a peer where it was active.
Minimal repro (attached repro.c)
Runs the identical scenario twice, differing only in whether a dummy entity keeps SysHigh active:
--- Scenario A: target alone (SysHigh initially DORMANT) ---
frame 3: target->count = 4 (delta this frame = 2 <<< DOUBLE-PROCESSED)
...
--- Scenario B: dummy present (SysHigh initially ACTIVE) ---
frame 3: target->count = 3 (delta this frame = 1)
...
==== RESULT ====
Total count Run A (SysHigh dormant): 7
Total count Run B (SysHigh active) : 6
>>> NON-DETERMINISTIC: identical per-frame inputs, different result (7 vs 6),
>>> solely because of whether SysHigh was already active (world history).
Questions
- Is a just-activated system running in the same frame (after a sibling that the now-transitioned entity previously matched) intended behavior?
- For deterministic sims, is there a supported way to make a system's presence in the pipeline schedule independent of whether its query currently matches — i.e., keep it always scheduled so the op structure is history-independent? (We tried
EcsQueryMatchEmptyTables, but flecs_pipeline_build still marks a system inactive when its matched tables are EcsEmpty.)
- Or is there a recommended pattern for LOD/rate-limited split systems that stays deterministic? (Our current fallback is to fold the variants into a single system that reads the "rate" as a component field instead of gating multiple systems by a tag, or to add a per-entity "already updated this frame" guard.)
Problem 2 — table iteration order is not stable across machines after deserialization
For determinism, every peer must iterate entities in the same order each frame (order affects any order-sensitive logic, and it affects our per-frame state hashing). After we deserialize a world snapshot on a client, the table order (and therefore the order in which a system's query visits tables/entities) can differ from the authoritative peer, because table creation order depends on the order components/tags were added during load, which isn't guaranteed to match.
Current workaround: we sort tables (impose a deterministic ordering on the tables a query visits) so iteration order is stable across peers. This works but feels like fighting the
framework.
Questions
- Is there a supported/first-class way to get a deterministic, machine-independent table iteration order for a query (e.g., a stable sort key,
order_by on a component, or a serialization mode that reproduces table order)?
- Does flecs guarantee anything about table order being reproducible after
ecs_world deserialization if entities/components are restored in the same order?
- Any recommended approach for deterministic iteration order in a lockstep/rollback context that avoids a manual per-frame table sort?
Thanks! Happy to provide more detail or additional repros. Overall flecs has been great to build on; these are the two spots where determinism needs extra care and we'd like to align with the intended approach.
Determinism concerns for lockstep/rollback simulation
flecs version: 4.1.3 (C, single-threaded pipeline)
We're building a deterministic lockstep simulation on flecs: multiple machines must produce bit-identical results given the same initial state + the same inputs. A client also restores a serialized world snapshot and re-simulates forward, and that re-simulation must match what the authoritative peer computed. We've hit two flecs behaviors that make this hard, and I'd like to know the recommended way to handle them (or whether they're considered bugs).
Problem 1 — a system that activates mid-frame runs in the same frame, so per-frame results depend on world history
We use a level-of-detail pattern that splits one logical per-entity update into mutually-exclusive systems gated by a tag, all in the same phase:
Both run in
EcsOnUpdate(SysLow first), and both do the same kind of per-entity work. The design assumes each entity is handled by exactly one of them per frame.A separate system in an earlier phase (
EcsPreUpdate) addsFastTagto an entity on some frame (like a relevance/interest system re-classifying it). The add is deferred, so it's still pending when theOnUpdatevariants run.Observed: whether the entity is processed once or twice on the transition frame depends only on whether
SysHighwas already active (i.e., whether any other entity already hadFastTag), because:SysHighis dormant (its query matches no non-empty table), addingFastTagmid-progress makes it match → the pipeline is rebuilt → the newly-activeSysHighruns in the same progress, afterSysLowalready processed the just-tagged entity → double-processed.SysHighwas already active, the entity is processed once.So the same per-frame inputs produce a different result depending on world history. In lockstep this desyncs: a client that restored a snapshot in which
SysHighhappened to be dormant computes a different result than a peer where it was active.Minimal repro (attached
repro.c)Runs the identical scenario twice, differing only in whether a dummy entity keeps
SysHighactive:Questions
EcsQueryMatchEmptyTables, butflecs_pipeline_buildstill marks a system inactive when its matched tables areEcsEmpty.)Problem 2 — table iteration order is not stable across machines after deserialization
For determinism, every peer must iterate entities in the same order each frame (order affects any order-sensitive logic, and it affects our per-frame state hashing). After we deserialize a world snapshot on a client, the table order (and therefore the order in which a system's query visits tables/entities) can differ from the authoritative peer, because table creation order depends on the order components/tags were added during load, which isn't guaranteed to match.
Current workaround: we sort tables (impose a deterministic ordering on the tables a query visits) so iteration order is stable across peers. This works but feels like fighting the
framework.
Questions
order_byon a component, or a serialization mode that reproduces table order)?ecs_worlddeserialization if entities/components are restored in the same order?Thanks! Happy to provide more detail or additional repros. Overall flecs has been great to build on; these are the two spots where determinism needs extra care and we'd like to align with the intended approach.