Cluster Dormancy (Sleep / Wake)
Clusters with no component writes for N ticks sleep and skip dispatch entirely, waking within one tick of being touched.
Status: โ Implemented ยท Visibility: Internal ยท Category: Spatial
๐ฏ What it solves
Even inside a coarse simulation tier, a large share of clusters do nothing tick after tick โ ants in empty space, idle props, units with no orders. Tiered Simulation Dispatch and its cell amortization already shrink the per-tick cluster set; Cluster Dormancy shrinks it further by letting the engine notice clusters that have gone quiet and stop dispatching them entirely, until something inside them actually changes again.
โ๏ธ How it works (in brief)
Each cluster carries a sleep counter that increments every tick its dirty-bitmap region is clean (no
component write landed in it) and resets to zero the moment a write does land. Once the counter reaches a
configurable per-archetype threshold, the cluster flips Active โ Sleeping and is dropped from every
system's dispatch list for that archetype โ tier-filtered or not โ at zero per-entity cost. A write to a
sleeping cluster during parallel system execution doesn't touch shared sleep state inline (that would race);
it instead records a deferred wake request on a thread-local list, drained single-threaded at the tick fence
(Sleeping โ WakePending) and promoted to Active at the very start of the next tick, before any system
dispatches โ one tick of latency end to end. An optional heartbeat interval wakes sleeping clusters anyway
on a staggered schedule, for a periodic idle re-check independent of writes.
๐ป Usage
// Dormancy is per-archetype and opt-in. SleepThresholdTicks / HeartbeatIntervalTicks live on the
// archetype's cluster runtime state (ArchetypeClusterState, internal) โ there is no public configuration
// wrapper yet, so setting them requires the same engine-internal access AntHill.Core already builds under.
var clusterState = dbe._archetypeStates[Archetype<Ant>.Metadata.ArchetypeId].ClusterState;
clusterState.SleepThresholdTicks = 120; // 60 Hz tick rate: 2s with no writes -> Sleeping
clusterState.HeartbeatIntervalTicks = 300; // wake briefly every 5s regardless, for an idle re-check
// From here on dormancy is fully automatic โ no per-system opt-in needed. Normal component writes
// (OpenMut().Write(...)) reset the sleep counter and wake a sleeping cluster; every QuerySystem against
// this archetype, tier-filtered or not, silently skips clusters that are Sleeping.
schedule.QuerySystem("IdleDrift", ctx =>
{
foreach (var id in ctx.Entities) { /* never dispatched against a sleeping cluster */ }
}, input: () => antsView, parallel: true, tier: SimTier.Tier2, cellAmortize: 60);
Field (ArchetypeClusterState) |
Default | Effect |
|---|---|---|
SleepThresholdTicks |
0 (disabled) |
Consecutive clean ticks before Active โ Sleeping; clamped to [0, 65535] |
HeartbeatIntervalTicks |
0 (off) |
When > 0, sleeping clusters wake on a staggered schedule for a periodic re-check |
โ ๏ธ Guarantees & limits
- Wake is bounded to one tick of latency โ a write to a sleeping cluster at tick T is guaranteed
WakePendingby T's tick fence andActivebefore any system dispatches at tick T+1; never permanently missed. - Zero overhead when nothing sleeps โ the dormancy filter in dispatch prep is skipped entirely
(
SleepingClusterCount == 0), for every system, every tick, until at least one cluster actually sleeps. - Activity is the dirty bitmap, not all writes โ
ClusterRef.WriteSpatial(the typical high-frequency path for a[SpatialIndex]-marked field, e.g. movement) deliberately does not mark the slot dirty, so it neither resets the sleep counter nor wakes a sleeping cluster. A cluster whose entities only move viaWriteSpatialcan go to sleep, and stay asleep, while still moving โ resetting the counter requires a normalTransaction.OpenMut().Write(...)on some other field, or an explicitSetDirtycall afterWriteSpatial. - Only two wake triggers exist today: a dirty write and the heartbeat timer. Proximity-based wake
("another entity approached") and tier-promotion wake ("camera moved closer") are not implemented โ a
sleeping cluster promoted to
Tier0staysSleeping, and excluded from dispatch, until a write or heartbeat wakes it. - Filters both tier-filtered and unfiltered systems โ a
SimTier.AllQuerySystemagainst the archetype still skips sleeping clusters; dormancy is not a tier-only mechanism. - Sleep counters are
ushort, clamped thresholds โSleepThresholdTicksis clamped to[0, 65535](~18 minutes at 60 Hz) so the counter can never wrap and falsely re-arm a sleep transition. - Cluster removal keeps the count honest โ destroying the last entity in a
SleepingorWakePendingcluster decrementsSleepingClusterCountwhen the cluster is removed from the active list. - No public configuration API yet โ
ArchetypeClusterStateis engine-internal; only theClusterSleepStateenum is part of the public surface. Configuring dormancy currently requires the host project to haveInternalsVisibleToaccess toTyphon.Engine(the same boundaryAntHill.Coreandtshbuild under).
๐งช Tests
- DormancyTests โ
SleepAfterThreshold/SleepingClusterSkippedInDispatch, deferredWakeRequest_Transition/DuplicateWakeDeduplication,HeartbeatWake,SetDirty_WakesSleepingClustervsWriteSpatialnot waking, cluster-removal count bookkeeping
๐ Related
- Source: src/Typhon.Engine/Ecs/internals/ArchetypeClusterState.cs (
DormancySweep,ProcessWakeRequest,TransitionWakePendingToActive,SetDirty) - Source: src/Typhon.Engine/Runtime/internals/DormancyReporter.cs (thread-local deferred wake requests)
- Source: src/Typhon.Engine/Runtime/public/TyphonRuntime.cs (
OnParallelQueryPreparedormancy filter,BuildTierIndexesAtTickStartwake transition) - Source: src/Typhon.Engine/Ecs/public/ClusterSleepState.cs
- Related catalog entry: Tiered Simulation Dispatch (the tier filter dormancy composes with)
- Related catalog entry: Spatially-Coherent Entity Clustering (
WriteSpatial, cluster/dirty-bitmap fundamentals) - Sibling: Cluster Dormancy (Sleep/Wake) โ same feature cataloged from the Runtime/dispatch angle rather than the spatial-grid angle