Struct ClusterSpatialAabb
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
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
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
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
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
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
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
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
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
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
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
Returns
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
Returns
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
Returns
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
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
entityMinXfloatentityMinYfloatentityMinZfloatentityMaxXfloatentityMaxYfloatentityMaxZfloatentityCategoryMaskuint
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
boxClusterSpatialAabbentityMinXfloatentityMinYfloatentityMaxXfloatentityMaxYfloatentityCategoryMaskuint
WidenCas3F(ref ClusterSpatialAabb, float, float, float, float, float, float, uint)
Union3F(float, float, float, float, float, float, uint) as a per-axis CAS — see WidenCas2F(ref ClusterSpatialAabb, 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)