Table of Contents

Struct ClusterRef<TArch>

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

Per-cluster accessor providing typed, zero-copy access to component SoA arrays. Created by ClusterEnumerator<TArch>. Must not outlive the enumerator.

public ref struct ClusterRef<TArch> where TArch : class

Type Parameters

TArch
Inherited Members

Remarks

Component data is laid out in Structure-of-Arrays format within the cluster: Component₀[N], Component₁[N], ... where N is the cluster size (8..64).

Iteration pattern using OccupancyBits TZCNT loop:

ulong bits = cluster.OccupancyBits;
while (bits != 0)
{
    int idx = BitOperations.TrailingZeroCount(bits);
    bits &= bits - 1;
    ref var pos = ref cluster.Get(Ant.Position, idx);
    // ...
}

Properties

ChunkId

The chunk ID of this cluster within the archetype's segment.

public readonly int ChunkId { get; }

Property Value

int

ClusterSize

Cluster size N (number of slots, 8..64).

public readonly int ClusterSize { get; }

Property Value

int

EntityIds

Entity keys for all N slots. Use with slot index to reconstruct EntityId.

public readonly ReadOnlySpan<long> EntityIds { get; }

Property Value

ReadOnlySpan<long>

FullMask

Full mask with lower N bits set.

public readonly ulong FullMask { get; }

Property Value

ulong

IsFull

True when all slots are occupied.

public readonly bool IsFull { get; }

Property Value

bool

LiveCount

Number of live entities in this cluster (PopCount of OccupancyBits).

public readonly int LiveCount { get; }

Property Value

int

OccupancyBits

Bitmask of occupied slots. Bit i = 1 means slot i contains a live entity.

public readonly ulong OccupancyBits { get; }

Property Value

ulong

Realm

The realm this cluster is in (Realms): every entity it holds is in that realm's frame, except one whose realm change awaits the next fence. A system scopes its spatial queries by it — dbe.ClusterSpatialQuery<T>(cluster.Realm). Realm 0 for an archetype without a realm key.

public readonly RealmId Realm { get; }

Property Value

RealmId

SpatialBounds

Tight AABB of all entities in this cluster, in world coordinates. Returns the empty sentinel (min = +inf, max = -inf) when the archetype has no spatial index, or when the cluster is not attached to a cell. For 2D archetypes, MinZ/MaxZ are ±infinity sentinels — use MinX/MinY/MaxX/MaxY only.

public ClusterWorldAabb SpatialBounds { get; }

Property Value

ClusterWorldAabb

Remarks

Returns a value, not a ref readonly, since #872 step 9. The engine stores these bounds C15 cell-relative, and this property is the boundary where they become world coordinates again. A reference cannot convert, and leaving it as one would have silently changed what every existing caller receives: the AntHill rock gather (TyphonBridge) and a demo renderer's camera cull both compared this box against world positions, so they would have kept compiling and started answering wrongly by exactly the distance to the cell's origin.

And it returns a DIFFERENT type since #914 — ClusterWorldAabb, six f64 components. Two reasons, and the second is the stronger one. It is f64 because a world coordinate is f64 now, and an f32 box would quantise to ~64-unit steps at 10⁹. It is a separate type because handing world values back in ClusterSpatialAabb made one struct mean two incompatible things — stored-and-cell-relative on one side of this property, world on the other — with nothing in the type to tell a caller which one it was holding.

The cost is a 56-byte copy plus two dependent loads for the cell origin, against returning a reference. That is the right trade for a public property: callers reading it per cluster per frame can afford it, and a caller that wants the raw stored frame is inside the engine and can use Typhon.Engine.ClusterRef`1.CellRelativeBounds.

Methods

ActiveBits(int)

Combined mask: alive AND component at slot enabled.

public readonly ulong ActiveBits(int slot)

Parameters

slot int

Returns

ulong

EnabledBits(int)

Bitmask of entities with component at slot enabled.

public readonly ulong EnabledBits(int slot)

Parameters

slot int

Returns

ulong

GetEntityId(int)

Read EntityId for the entity at the given slot (stored as full packed EntityId).

public readonly EntityId GetEntityId(int slotIndex)

Parameters

slotIndex int

Returns

EntityId

GetReadOnlySpan<T>(Comp<T>)

Get a read-only span of component data for all N slots.

public readonly ReadOnlySpan<T> GetReadOnlySpan<T>(Comp<T> comp) where T : unmanaged

Parameters

comp Comp<T>

Returns

ReadOnlySpan<T>

Type Parameters

T

GetReadOnly<T>(Comp<T>, int)

Get a read-only reference to a single component value at the given slot index.

public ref readonly T GetReadOnly<T>(Comp<T> comp, int slotIndex) where T : unmanaged

Parameters

comp Comp<T>
slotIndex int

Returns

T

Type Parameters

T

GetSpan<T>(Comp<T>)

Get a mutable span of the component's data across all N slots (its SoA array). For Versioned components use GetReadOnlySpan<T>(Comp<T>) instead — writing directly to the cluster slot bypasses the revision chain and breaks MVCC snapshot isolation.

public readonly Span<T> GetSpan<T>(Comp<T> comp) where T : unmanaged

Parameters

comp Comp<T>

Handle identifying the component within the archetype.

Returns

Span<T>

A mutable span of length ClusterSize over the component's SoA array.

Type Parameters

T

Component value type.

Remarks

Refused for a component with an [Index] field the tick fence maintains (SingleVersion or Transient): the index moves an entity's entry from the key recorded at its first write, and a write through handed-out memory records nothing, so the index would keep the old key for good (#1187, IX-07). Read such a component with GetReadOnlySpan<T>(Comp<T>) and write it with Set<T>(Comp<T>, int, in T) or OpenMut(...).Set.

Exceptions

InvalidOperationException

The component has a fence-maintained indexed field, in every build; or, when strict checks are enabled (Enabled), T is a Versioned component.

Get<T>(Comp<T>, int)

Get a mutable reference to a single component value at the given slot index.

public readonly ref T Get<T>(Comp<T> comp, int slotIndex) where T : unmanaged

Parameters

comp Comp<T>
slotIndex int

Returns

T

Type Parameters

T

Remarks

Refused, in every build, for a component with a fence-maintained indexed field, as GetSpan<T>(Comp<T>) is (#1187).

MarkDirty<T>(Comp<T>)

Mark every occupied slot in this cluster dirty for ONE component column — the columnar counterpart to what EntityRefMut.Write does per entity.

Required after writing through GetSpan<T>(Comp<T>) for any durable (SingleVersion) component. The direct cluster path sets no dirty bits, so without this the tick fence never serialises the change and it is lost on reopen. Transient components need no call — they are never persisted.

Prefer this over ClusterEnumerator.MarkCurrentDirty(), which cannot name a component and therefore widens the archetype's written-slot union to "all slots" — that union is what narrows the columns a FenceBlock emits (#559), so losing it inflates WAL volume for every entity in the archetype.

public readonly void MarkDirty<T>(Comp<T> comp) where T : unmanaged

Parameters

comp Comp<T>

Handle identifying the component column that was written.

Type Parameters

T

Component value type.

Set<T>(Comp<T>, int, in T)

Store one entity's value of a component — the cluster-iteration counterpart of EntityRefMut.Set, and the way to write a component whose indexed fields the tick fence maintains, which GetSpan<T>(Comp<T>) and Get<T>(Comp<T>, int) refuse (#1187).

public readonly void Set<T>(Comp<T> comp, int slotIndex, in T value) where T : unmanaged

Parameters

comp Comp<T>

Handle identifying the component within the archetype.

slotIndex int

The entity's slot: a bit of OccupancyBits.

value T

The value to store.

Type Parameters

T

Component value type.

Remarks

Does the in-place write's bookkeeping in the same call, as EntityRefMut.Set does: on the entity's first write this tick it records the keys of its indexed fields for the fence to move them from (IX-07), marks the entity dirty for the written component, and records the page owing a write (PS-10). No MarkDirty<T>(Comp<T>) is needed.

Exceptions

InvalidOperationException

The component is Versioned or defaults to the Commit discipline (write it with OpenMut), is the spatial component (WriteSpatial<T>(Comp<T>, int, in T)) or holds the realm key (OpenMut, RM-05), or it is SingleVersion and the enumerating transaction runs under the Commit discipline, whose writes are staged.

ArgumentException

slotIndex holds no entity.

Set<T>(Comp<T>, ulong, scoped ReadOnlySpan<T>)

Store several entities' values of a component in one call: Set<T>(Comp<T>, int, in T) for every slot of slots, with values indexed by slot. Its bookkeeping is per cluster rather than per entity — one shadow word, one dirty word, one page note (#1346) — so a pass writing most of a cluster's entities pays it once.

public readonly void Set<T>(Comp<T> comp, ulong slots, scoped ReadOnlySpan<T> values) where T : unmanaged

Parameters

comp Comp<T>

Handle identifying the component within the archetype.

slots ulong

The entities' slots: bits of OccupancyBits.

values ReadOnlySpan<T>

The values, by slot: values[i] is stored at slot i. Long enough for the highest slot of slots.

Type Parameters

T

Component value type.

Exceptions

InvalidOperationException

As Set<T>(Comp<T>, int, in T).

ArgumentException

A slot of slots holds no entity, or values is too short.

WriteSpatial<T>(Comp<T>, int, in T)

Write-barrier API for spatial components — the canonical replacement for cluster.GetSpan<T>()[slotIndex] = ... when T contains the archetype's SpatialIndexAttribute-marked field. Performs (in order):

  1. Reads the OLD spatial-field bytes at slotIndex
  2. Writes newValue to the slot
  3. Updates Typhon.Engine.Internals.ArchetypeClusterState.ClusterAabbs inline on AABB grow (O(1) CAS per axis)
  4. Flags Typhon.Engine.Internals.ArchetypeClusterState.ClusterShrinkPendingAxes for axes where this slot was at an extreme and moved inward — fence rescans only this cluster on those axes
  5. Flags Typhon.Engine.Internals.ArchetypeClusterState.ClusterMigrationPendingSlots when the new position crosses the cell+hysteresis boundary — fence drains the migration without any full scan
  6. Sets the cluster's bit in Typhon.Engine.Internals.ArchetypeClusterState.ClusterProcessBitmap so the fence loop visits this cluster

All eight SpatialFieldType variants are supported — the four f32 tiers since #914 phase B, the four f64 ones since phase C. V1 handled AABB2F alone (AntHill's WorldBounds), which is why a 3D archetype used to fall back to the MVCC path and pay one WAL frame per entity per tick. Each tier has its own decode specialization; they all hand f64 world bounds to one shared ApplySpatialWrite, whose is3D is a call-site constant so the 2D path costs what it always did.

WriteSpatial does NOT mark the slot dirty (via SetDirty(int, int, int)), unless the value changes an [Index] field of the spatial component: that is a key move, recorded and marked dirty as any write's is, for the fence to move the index entry (#1187). The dirty bitmap drives WAL serialization and change-filtered dispatch — for high-frequency simulation state (e.g., AntHill's ant positions), marking every slot dirty floods the WAL writer with one frame per entity per tick → backpressure that stalls TickDriver. The fence-time spatial maintenance does not need the dirty bit; it consumes Typhon.Engine.Internals.ArchetypeClusterState.ClusterMigrationPendingSlots / Typhon.Engine.Internals.ArchetypeClusterState.ClusterProcessBitmap directly. If your workload genuinely needs WAL persistence of the spatial field (e.g., resumable autosave), either write through the MVCC Transaction.OpenMut + Write path (which marks dirty), or call SetDirty(int, int, int) explicitly after WriteSpatial.

Thread safety: safe to call concurrently from multiple workers operating on different slots of any cluster (including the same cluster). All bookkeeping writes use Interlocked primitives.

public void WriteSpatial<T>(Comp<T> comp, int slotIndex, in T newValue) where T : unmanaged

Parameters

comp Comp<T>
slotIndex int
newValue T

Type Parameters

T

WriteSpatial<T>(Comp<T>, ulong, scoped ReadOnlySpan<T>)

The per-cluster form of the barrier: writes newValues[i] to every slot i set in slots, and does the bookkeeping once for the cluster rather than once per entity.

public void WriteSpatial<T>(Comp<T> comp, ulong slots, scoped ReadOnlySpan<T> newValues) where T : unmanaged

Parameters

comp Comp<T>
slots ulong
newValues ReadOnlySpan<T>

Type Parameters

T

Remarks

It leaves the cluster as the same single-slot writes would, made in ascending slot order on one thread: slot values, cluster bound, shrink and migration flags, destination hint, counters, process bit (rule CA-03). Each slot is tested against the bound as the writes before it in the call have grown it, which is what the single write finds there. A writer running on the same cluster meanwhile, a spawn or a write to another of its slots, is seen by single writes as they go and by this call only when it publishes, so the shrink flags and the process bit can differ. Containment cannot: the bound still covers every entity, and a widening this call leaves unflagged was indexed by the writer that made it.

A slot whose position is not finite stops the call where the single write would throw: the slots before it and that slot's own grow are published, the slots after it are not written, and the call throws the single write's exception. The process bit is then set when that slot's grow changed the bound, where the single write leaves it clear.

Why it is cheaper. The single write dispatches on the field type, converts eight world bounds into the cell's frame, reads the growth stamp and the stored bound, and may CAS the bound and OR three bitmaps, all per entity. Here the field type is dispatched once; the new boxes are united in world space and a bound converted only when a box extends the union, exact because the conversion is monotonic; an old box is converted only on an axis where its entity moved inward; and each bitmap is written at most once.

newValues is indexed by slot, so it must be longer than the highest slot set: a ClusterSize-element stack buffer filled for the moved slots is the intended caller. As with the single write, the slots should be live ones, and they are not marked dirty.

Exceptions

ArgumentException

A slot of slots is beyond the cluster or beyond newValues.