Determinism, Snapping, and Pooling
This page covers the three invariants that shape most of GridForge's implementation choices:
- deterministic math and ordering
- snapped spatial boundaries
- aggressive object and collection reuse
Determinism Is A Design Goal
GridForge is built around fixed-point math and explicit ordering.
In practice that means:
- core spatial math uses
Fixed64,Vector2d, andVector3d - grid creation and tracing logic work from snapped fixed-point bounds
- behavior must stay stable across both
netstandard2.1andnet10.0
Vector2d APIs use the same fixed-point math as the 3D APIs. They project XZ
coordinates onto a chosen layerY and then flow through the same world, grid,
voxel, tracer, scan, obstacle, and blocker systems.
Snapping Starts At Registration Time
GridConfiguration orders incoming bounds on construction, and GridWorld
snaps them during registration.
That snapped result affects:
- grid dimensions
- duplicate grid detection
- world-space containment tests
- tracer coverage
- blocker coverage
- local voxel index resolution
Cell Metrics Are A Grid-Level Assumption
GridConfiguration.TopologyMetrics establishes deterministic cell geometry for
the grid being registered.
That means:
- grid configuration snapping depends on the normalized topology metrics
- voxel index math for that grid depends on those metrics
- changing cell geometry is a grid-configuration choice, not a hidden world-wide scalar
When tests or tools need different cell geometry, create the grid with explicit topology metrics and keep expectations local to that grid.
Hex Projection And Rounding
Hex centers use a quantized fixed-point basis: the full width is
CellRadius * Sqrt3, and the row step is CellRadius * 3 * Half, evaluated in
that order. World-to-axial projection divides by these same quantized values. An
ideal irrational inverse is not interchangeable with this inverse, especially
for small positive raw radii or distant addresses.
The forward projection retains FixedMathSharp's round-half-to-even behavior. An odd raw width on an odd row can round a center by half a raw unit, so the continuous inverse need not be an exact integer. Lookup uses deterministic cube rounding; bounds normalization recognizes exactly projected axial corners before applying its existing outward ceiling/tolerance rule. Authored corners therefore preserve their address dimensions when normalized repeatedly.
These guarantees require representable, unsaturated intermediate arithmetic: the
basis (including CellRadius * 3), axial projection, translated centers,
relative offsets, inverse coordinates, and cube coordinate -q - r must fit
Fixed64. Saturation is not an invertible coordinate mapping. No minimum radius
beyond a positive representable value is imposed; individual geometry queries
can have stricter prism-representability checks. Normalized address counts
remain limited to Int32.MaxValue, including sparse grids. This correction
preserves forward centers, but grids previously affected by inverse drift can
resolve different indices or normalize to different bounds; rebuild derived
dimensions and configuration keys from the authored input.
Runtime Generations Validate Reuse
Pooling makes storage slots reusable, so GridIndex and OccupantTicket.Slot
are never sufficient identities by themselves. An active world receives a
process-unique token, each grid registration receives a nonrepeating generation
local to that world, and each occupant registration receives a process-unique
generation. WorldVoxelIndex and OccupantTicket carry those values so stale
references fail instead of aliasing replacement state.
These generations are transient validation metadata. They are not serialized,
durable across processes, or authoritative ordering inputs. Persist host-owned
content identity such as IVoxelOccupant.GlobalId, then resolve fresh runtime
handles when rebuilding a world.
Pooling Is A First-Class Constraint
Pooling in GridForge is not an optimization sprinkled on top. It shapes the object lifecycle.
The internal pools cover types such as:
VoxelGridVoxelScanCell- scan-cell maps
- neighbor arrays
- temporary query lists and hash sets
Every new mutable field introduced into a pooled type needs a matching reset story.
Practical Debugging Checklist
- Was the grid created with the topology metrics you think it was?
- What are the snapped bounds after normalization?
- Is the queried world-space position exactly on a boundary?
- Are you looking at a pooled object or temporary collection after its intended lifetime?
- Did a previous test, tool run, or benchmark leave world state active longer than intended?