Table of Contents

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 Versioned write.
  • Crash recovery to the last completed WriteTickFence call โ€” up to one tick of writes can be lost, but state is never torn or corrupted.
  • Forgetting to call WriteTickFence silently degrades a SingleVersion component to Transient-like durability (no crash recovery) โ€” it never corrupts data.
  • No MVCC isolation: last-writer-wins, and tx.Rollback() does not revert a SingleVersion write already applied in-place โ€” unless the transaction was opened with the Commit discipline, which stages the write and therefore does roll it back, in O(1). See the sub-feature below.
  • ReadsSnapshot is rejected at scheduler Build() time for SingleVersion components โ€” use Versioned for 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 SingleVersion write 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: Rollback reverts it, the write is all-or-nothing, and the โ‰ค1-tick loss window closes.

๐Ÿงช Tests

  • StorageModeTickFenceTests โ€” WriteTickFence dirty-bitmap serialization, Versioned/Transient correctly skipped
  • TickFenceE2ETests โ€” crash/reopen recovery to the last completed tick fence, multi-entity and multi-update recovery
  • 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