Table of Contents

Struct ClusterSpatialAabb

Namespace
Typhon.Engine
Assembly
Typhon.Engine.dll

Per-cluster tight AABB plus category mask, used by the per-cell cluster spatial index (issue #230). One instance per spatially-active cluster, indexed by clusterChunkId. Stored in-memory only on Typhon.Engine.Internals.ArchetypeClusterState and rebuilt at startup via RebuildClusterAabbs from entity positions (Q2/Q6 transient-state decision).

public struct ClusterSpatialAabb
Inherited Members

Remarks

The six bounds are CELL-RELATIVE, not world-space (#872 step 9, decision C15). They are offsets from the world-space minimum corner of the cell the cluster belongs to, which SpatialGrid.CellOrigin derives from the cluster's entry in ClusterCellMap. A cluster lives wholly inside one cell (C13), so that origin is unambiguous — and it is why a cluster's bounds must be REBASED when the cluster migrates to another cell. A bound left un-rebased is off by exactly one cell size, which is a silent SQ-01 false negative rather than an error.

Why not world space. f32 across a ±10⁹ world resolves to ~64 units; measured against a cell the magnitude is bounded by the cell size, and the same 24 mantissa bits resolve ~6 × 10⁻⁵. Note the limit this does NOT lift: an f32 tier's own spatial component is world-space f32, so at extreme magnitudes the source coordinate is already coarse and cell-relative storage cannot recover precision the input never carried. C15 buys resolution for DERIVED bounds — this AABB and the R-Tree's node bounds — not for the component field they are computed from. The answer to that limit is not to widen this struct: it is to declare the archetype at an f64 tier (AABB2D/AABB3D), whose component carries the magnitude and whose query box and result carry it back out (#914). The stored bound stays exactly as wide as it is here either way.

Conversion goes through ToCellRelativeMin(double, double) / ToCellRelativeMax(double, double), never a bare subtraction: the narrowing to f32 rounds, and rounding the wrong way puts a bound inside the entity it must contain.

Storage shape. 28 bytes: six f32 bounds components (XYZ min/max) plus a 4-byte category mask. 2D archetypes leave MinZ/MaxZ at the Empty sentinel (+inf/-inf); 2D queries use an infinite Z range which trivially passes the Z overlap test, so 2D clusters match correctly. 3D archetypes populate all six bounds. The unified 3D storage adds ~8 bytes per cluster versus a 2D-only design, in exchange for a single cluster-index code path that handles both tiers. The f64 tiers (AABB2D/AABB3D and their BSphere forms) are supported since #914 and are stored here unchanged — as f32 CELL-RELATIVE bounds, which is the point of C15 rather than a compromise with it.

The CategoryMask is the OR of all entity category masks in the cluster — it lets the per-cell broadphase skip entire clusters when the query's category mask does not intersect. Maintained incrementally on spawn and on migration. The fence's full recompute does NOT tighten it: the category is an archetype constant rather than per-entity geometry, so there is nothing to re-derive, and the recompute deliberately reads the stored value back and preserves it (ArchetypeClusterState.ReadStoredCategoryMask, asserted by ClusterSpatialAabbRecomputeTests.TickFence_CategoryMaskPreservedAcrossRecompute). The bounds beside it are re-derived; this field is carried through.

Fields

CategoryMask

OR of every entity category mask in the cluster. Lets the per-cell broadphase skip the whole cluster when the query's category mask does not intersect.

public uint CategoryMask

Field Value

uint

MaxX

Maximum X bound of the cluster's tight AABB, RELATIVE to its cell's world-space minimum corner (C15).

public float MaxX

Field Value

float

MaxY

Maximum Y bound of the cluster's tight AABB, RELATIVE to its cell's world-space minimum corner (C15).

public float MaxY

Field Value

float

MaxZ

Maximum Z bound, RELATIVE to its cell's world-space minimum corner (C15). Left at the Empty sentinel (-inf) for 2D archetypes.

public float MaxZ

Field Value

float

MinX

Minimum X bound of the cluster's tight AABB, RELATIVE to its cell's world-space minimum corner (C15).

public float MinX

Field Value

float

MinY

Minimum Y bound of the cluster's tight AABB, RELATIVE to its cell's world-space minimum corner (C15).

public float MinY

Field Value

float

MinZ

Minimum Z bound, RELATIVE to its cell's world-space minimum corner (C15). Left at the Empty sentinel (+inf) for 2D archetypes.

public float MinZ

Field Value

float

Properties

Empty

Create an empty AABB suitable as the seed for incremental unions (min = +inf, max = -inf on all axes).

public static ClusterSpatialAabb Empty { get; }

Property Value

ClusterSpatialAabb

Methods

CasMax(ref float, float)

CAS-loop float max update: write candidate if it is still greater than the stored value.

public static void CasMax(ref float storedRef, float candidate)

Parameters

storedRef float
candidate float

CasMin(ref float, float)

CAS-loop float min update: write candidate if it is still less than the stored value.

public static void CasMin(ref float storedRef, float candidate)

Parameters

storedRef float
candidate float

ToCellRelativeMax(double, double)

Convert a world-space UPPER bound to cell-relative space, rounding away from the entity. See ToCellRelativeMin(double, double).

public static float ToCellRelativeMax(double worldValue, double cellOrigin)

Parameters

worldValue double
cellOrigin double

Returns

float

ToCellRelativeMin(double, double)

Convert a world-space LOWER bound to cell-relative space, rounding AWAY from the entity so the result can only ever be too low (C15).

public static float ToCellRelativeMin(double worldValue, double cellOrigin)

Parameters

worldValue double
cellOrigin double

Returns

float

Remarks

The subtraction is done in double and the narrowing is what rounds. That is not defensive symmetry — it is where the error actually is. The bounds reaching this method come from SpatialMaintainer.ReadAndValidateBoundsFromPtr, which produces double, and even for an f32 source the exact difference need not be representable in f32: with an origin of 0.1 and a coordinate of 10⁷ the true offset needs more mantissa than f32 has, so (float) rounds it — and round-to-nearest rounds UP half the time, putting the lower bound INSIDE the entity it is supposed to contain.

That is a CA-01 violation, and CA-01's own on_violation says what it looks like from outside: "AABB too tight → per-cell cluster spatial queries miss entities (false negatives, silent)". One conditional BitDecrement(float) removes the whole class. The test that pins it is an ablation to plain (float) narrowing at large magnitudes.

ToWorldExact(float, double)

Convert a cell-relative bound to an f64 world coordinate — the conversion the f64 world frame wants, and the one every world-space read-back uses since #914.

public static double ToWorldExact(float cellRelativeValue, double cellOrigin)

Parameters

cellRelativeValue float
cellOrigin double

Returns

double

Remarks

One method covers both min and max, and the reason is a size argument rather than a claim of perfection. The f32-returning variants this replaced needed a direction because the NARROWING is what rounds, and rounding the wrong way moves a bound INSIDE the entity it must contain (CA-01). Here nothing narrows: the sum is formed and returned in double.

How exact, precisely. The addition is exact whenever the result fits in 53 bits, and for a bound produced by this engine it does: the offset is an f32 bounded by one cell (24 bits of mantissa) and the origin is worldMin + cellIndex × cellSize with the index bounded to 21 bits by VdbBlockKey — 21 + 24 = 45, and the cell size cancels out of that sum, so it holds at any cell size. What is NOT covered is a worldMin that itself spends the mantissa (an origin of 2³⁶ + 12345.678, say); there the sum rounds to nearest by up to half a double ULP.

Why that residual cannot break CA-01 or SQ-01. Half a double ULP at 2³⁶ is ~4 × 10⁻⁶, while the stored bound reaching this method was already rounded OUTWARD by up to one f32 ULP in the cell frame — ~6 × 10⁻⁵ across a 1 000-unit cell, an order of magnitude larger. So a converted bound stays outside the entity it contains even in the worst case, which is what the ray's face test and kNN's lower bound rely on.

This is the only conversion out of the stored frame. Three f32-returning variants stood here until #919 — an undirected ToWorld for display and a directed ToWorldMin/ToWorldMax pair for the ray and kNN paths. All three lost their last production caller when the query shapes went f64, and leaving them would have left three ways to quantise a world coordinate to ~64-unit steps at 10⁹ on the public surface of the type whose whole job is the opposite. The compounding hazard the old ToWorld warned about does not arise here either: these values are consumed and discarded, never re-stored.

Union2F(float, float, float, float, uint)

Union a 2D entity's tight AABB + category mask into this cluster AABB in place. Leaves MinZ/MaxZ at their initial values; 2D cluster archetypes never populate Z bounds, and 2D queries against those clusters use an infinite Z range that trivially passes the Z overlap test regardless of the stored Z values.

public void Union2F(float entityMinX, float entityMinY, float entityMaxX, float entityMaxY, uint entityCategoryMask)

Parameters

entityMinX float
entityMinY float
entityMaxX float
entityMaxY float
entityCategoryMask uint

Union3F(float, float, float, float, float, float, uint)

Union a 3D entity's tight AABB + category mask into this cluster AABB in place. Updates all six bounds components.

public void Union3F(float entityMinX, float entityMinY, float entityMinZ, float entityMaxX, float entityMaxY, float entityMaxZ, uint entityCategoryMask)

Parameters

entityMinX float
entityMinY float
entityMinZ float
entityMaxX float
entityMaxY float
entityMaxZ float
entityCategoryMask uint

WidenCas2F(ref ClusterSpatialAabb, float, float, float, float, uint)

Union2F(float, float, float, float, uint) for a box other threads are widening at the same time: every axis is a CAS loop that only ever moves a min down or a max up, so two spawns into one cluster cannot lose each other's widening the way the plain read-modify-write could (step 15 review, CA-01).

public static void WidenCas2F(ref ClusterSpatialAabb box, float entityMinX, float entityMinY, float entityMaxX, float entityMaxY, uint entityCategoryMask)

Parameters

box ClusterSpatialAabb
entityMinX float
entityMinY float
entityMaxX float
entityMaxY float
entityCategoryMask uint

WidenCas3F(ref ClusterSpatialAabb, float, float, float, float, float, float, uint)

public static void WidenCas3F(ref ClusterSpatialAabb box, float entityMinX, float entityMinY, float entityMinZ, float entityMaxX, float entityMaxY, float entityMaxZ, uint entityCategoryMask)

Parameters

box ClusterSpatialAabb
entityMinX float
entityMinY float
entityMinZ float
entityMaxX float
entityMaxY float
entityMaxZ float
entityCategoryMask uint