SingleVersion (Tick-Fence Durability)
In-place writes at near-zero cost, durable to the last completed game tick.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ต Core ยท Category: Ecs
๐ฏ What it solves
High-frequency component data โ position, velocity, health, cooldowns โ gets rewritten every tick by every
entity. Paying Versioned's copy-on-write and revision-chain cost on every such write would dominate the frame
budget for data that naturally tolerates losing the last few milliseconds on a crash. SingleVersion gives
near-Flecs/DOTS write performance while remaining on disk and recoverable, just to a coarser durability
boundary than Versioned.
โ๏ธ How it works (in brief)
A SingleVersion component has exactly one HEAD slot per entity โ writes overwrite it in place, last-writer-
wins, immediately visible to every reader (no isolation). Each write sets a bit in a per-entity dirty bitmap. At
the end of each game tick, DatabaseEngine.WriteTickFence(tickNumber) serializes every dirty SingleVersion
entity to the WAL as a tick-fence record, establishing a crash-recovery boundary. A crash recovers state as of
the last completed tick fence โ at most one tick of writes is lost, never corrupted (the WAL record holds
complete post-tick values and overwrites any torn on-disk state).
๐ป Usage
[Component("Game.Position", 1, StorageMode = StorageMode.SingleVersion)]
public struct Position
{
[Index] public int Zone;
public float X, Y, Z;
}
[Archetype]
partial class Unit : Archetype<Unit>
{
public static readonly Comp<Position> Pos = Register<Position>();
}
using var tx = dbe.CreateQuickTransaction();
var id = tx.Spawn<Unit>(Unit.Pos.Set(new Position { X = 0, Y = 0, Z = 0 }));
tx.Commit();
using var tx2 = dbe.CreateQuickTransaction();
var e = tx2.OpenMut(id);
ref var pos = ref e.Write(Unit.Pos);
pos.X += dtVelocityX; // in-place โ visible to every reader immediately, no commit needed for visibility
tx2.Commit();
// Once per game tick, after all systems have run:
dbe.WriteTickFence(tickNumber); // batches every dirty SingleVersion component to WAL โ the crash-recovery boundary
โ ๏ธ Guarantees & limits
- Write cost ~40 ns โ an in-place store into the pinned page (no allocation, no revision chain); ~6ร cheaper than a
Versionedwrite. - Crash recovery to the last completed
WriteTickFencecall โ up to one tick of writes can be lost, but state is never torn or corrupted. - Forgetting to call
WriteTickFencesilently degrades aSingleVersioncomponent toTransient-like durability (no crash recovery) โ it never corrupts data. - No MVCC isolation: last-writer-wins, and
tx.Rollback()does not revert aSingleVersionwrite already applied in-place. ReadsSnapshotis rejected at schedulerBuild()time forSingleVersioncomponents โ useVersionedfor snapshot reads.- Secondary B+Tree indexes and spatial structures are reconciled at the tick-fence boundary (deferred), not synchronously on every write.
- Need atomicity and zero loss for one write without paying for snapshot isolation? See
Committed Durability Discipline โ it escalates a
SingleVersionwrite to commit-time durability, closing the โค1-tick loss window.
๐งช Tests
- StorageModeTickFenceTests โ
WriteTickFencedirty-bitmap serialization, Versioned/Transient correctly skipped - TickFenceE2ETests โ crash/reopen recovery to the last completed tick fence, multi-entity and multi-update recovery
๐ Related
- Code:
src/Typhon.Engine/Ecs/internals/DirtyBitmap.cs,src/Typhon.Engine/Ecs/internals/DirtyBitmapRing.cs - Sub-feature: Committed Durability Discipline
- Sibling: Durability Modes โ the separate UoW-level commit-durability spectrum; tick-fence durability here is a distinct, component-level mechanism
- Parent feature: Storage Modes