-
-
Notifications
You must be signed in to change notification settings - Fork 0
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
If a change breaks one of these, it usually breaks more than one subsystem at once.
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
- build configuration enables deterministic compilation
- the library targets both
netstandard2.1andnet8.0, so behavior must stay stable across both
This is why introducing casual float or double math into core spatial logic is risky even when it looks convenient.
Determinism is not only about numeric values. It also includes traversal order.
One example already protected by tests is voxel neighbor enumeration:
-
Voxel.GetNeighbors(...)followsSpatialAwareness.DirectionOffsets - interior voxel neighbor order is expected to match that fixed direction ordering
- cached and uncached neighbor resolution are expected to agree
That is the kind of behavior that can be accidentally destabilized by "small cleanup" refactors.
GridConfiguration does not preserve arbitrary incoming bounds exactly as written.
Its constructor routes bounds through GlobalGridManager.SnapBoundsToVoxelSize(...), which:
- optionally applies padding
- floors the min corner to voxel size
- ceils the max corner to voxel size
- fixes ordering if min and max arrive inverted
That snapped result becomes the configuration's stored bounds and the source of its BoundsKey.
Snapped bounds affect:
- grid dimensions
- duplicate grid detection
- world-space containment tests
- tracer coverage
- blocker coverage
- local voxel index resolution
If a query result looks off by one voxel, snapped bounds are one of the first things to inspect.
GlobalGridManager.Setup(...) establishes the active voxel size for the session.
That means:
- all later grid configuration snapping depends on that value
- all later voxel index math depends on that value
- changing voxel size mid-session is not a local tweak
When tests or tools need a different voxel size, they should reset the world first and treat the next setup as a fresh runtime.
GridForge treats the snapped max bounds as inclusive for voxel allocation.
That is why grid dimension math uses:
((max - min) / voxelSize).FloorToInt() + 1
The extra + 1 is what lets a 1x1x1 style grid stay valid and is also why exact max-bound positions can still resolve to a voxel.
Once a grid exists, VoxelGrid gives you grid-local helpers:
FloorToGrid(...)CeilToGrid(...)TryGetVoxelIndex(...)SnapToScanCell(...)
These helpers snap relative to the grid's own bounds and clamp results back into the valid local range. That makes them safer than ad hoc position math when you already know the target grid.
The test suite explicitly covers:
- negative positions
- fractional voxel sizes
- exact max-bound inclusion
- outside-bound rejection
That is a strong hint about intended behavior: these are not edge cases to hand-wave away, they are part of the supported spatial model.
Pooling in GridForge is not an optimization sprinkled on top. It shapes the object lifecycle.
The internal GridForge pools currently cover types such as:
VoxelGridVoxelScanCell- scan-cell maps
- neighbor arrays
- occupant dictionaries and buckets
Shared SwiftCollections pools are also used for temporary query lists and hash sets.
Because pooled objects are reused, reset paths have to be complete.
Examples already enforced by code and tests:
-
VoxelGrid.Reset()releases voxels, scan cells, active scan-cell sets, neighbors, and summary state -
Voxel.Reset(...)is expected to clear obstacle state, partition state, occupant-facing state, and neighbor caches -
ScanCell.Reset()removes occupant ownership, releases buckets, and returns dictionaries to pools - pooled grids and voxels should not leak old neighbors, partitions, obstacles, or occupants into later allocations
This is one of the most important maintenance rules in the repo: every new mutable field introduced into a pooled type needs a matching reset story.
Some query paths rent temporary collections internally and release them after enumeration completes.
That means:
- consume tracer coverage results immediately
- avoid storing pooled lists or assuming ownership unless the API clearly hands it to you
- be careful when building new APIs on top of pooled internals
If ownership is unclear, assume the safe answer is "use it now, do not retain it."
The benchmark suite already measures cold vs warm behavior for some operations.
That matters because:
- cold-pool timings often include allocation and first-use overhead
- warm-pool timings are closer to steady-state runtime behavior
- a change that looks fine in warm conditions can still regress allocation pressure badly during startup or bursty workloads
This is why performance work in GridForge usually needs both correctness tests and benchmark confirmation.
- introducing
floatordoublemath into core grid calculations - assuming unsnapped input bounds are the real stored bounds
- forgetting that max bounds are inclusive
- holding pooled references past their intended lifetime
- adding state to pooled objects without clearing it in
Reset() - changing traversal order in a hot path without realizing callers or tests rely on it
When a spatial result looks wrong, check these in order:
- Was
GlobalGridManager.Setup()called with the voxel size you think it was? - What are the snapped bounds after
GridConfigurationnormalization? - 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 shared global state active?
That sequence solves a surprising number of "mystery" issues quickly.
- Core Concepts for the system vocabulary this page assumes
- GridTracer and Coverage for how snapped world-space input becomes covered cells
- Testing and Benchmarking for how these invariants are validated in practice