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 โ unless the transaction was opened with theCommitdiscipline, which stages the write and therefore does roll it back, in O(1). See the sub-feature below. 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 rollback, atomicity, or zero loss on one
SingleVersionwrite without paying for snapshot isolation? See Commit Discipline. Because it stages the write instead of applying it in place, it buys all three at once:Rollbackreverts it, the write is all-or-nothing, and the โค1-tick loss window closes.
๐งช 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: Commit 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