Skip to content

Determinism Snapping and Pooling

DESKTOP-5KM6RA3\david edited this page Apr 7, 2026 · 3 revisions

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.

Determinism Is A Design Goal, Not A Side Effect

GridForge is built around fixed-point math and explicit ordering.

In practice that means:

  • core spatial math uses Fixed64, Vector2d, and Vector3d
  • grid creation and tracing logic work from snapped fixed-point bounds
  • build configuration enables deterministic compilation
  • the library targets both netstandard2.1 and net8.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.

Deterministic Ordering Matters Too

Determinism is not only about numeric values. It also includes traversal order.

One example already protected by tests is voxel neighbor enumeration:

  • Voxel.GetNeighbors(...) follows SpatialAwareness.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.

Snapping Starts At Configuration Time

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.

Why Snapped Bounds Matter So Much

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.

Global Voxel Size Is A Session-Level Assumption

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.

Inclusive Max Bounds

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.

Grid-Local Snapping Helpers

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.

Negative And Fractional Coordinates Are Part Of The Contract

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 Is A First-Class Constraint

Pooling in GridForge is not an optimization sprinkled on top. It shapes the object lifecycle.

The internal GridForge pools currently cover types such as:

  • VoxelGrid
  • Voxel
  • ScanCell
  • scan-cell maps
  • neighbor arrays
  • occupant dictionaries and buckets

Shared SwiftCollections pools are also used for temporary query lists and hash sets.

Why Reset Logic Matters So Much

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.

Query Results Can Be Transient

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."

Pool Warmth Changes Performance Characteristics

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.

Common Invariant Breakers

  • introducing float or double math 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

Practical Debugging Checklist

When a spatial result looks wrong, check these in order:

  1. Was GlobalGridManager.Setup() called with the voxel size you think it was?
  2. What are the snapped bounds after GridConfiguration normalization?
  3. Is the queried world-space position exactly on a boundary?
  4. Are you looking at a pooled object or temporary collection after its intended lifetime?
  5. Did a previous test, tool run, or benchmark leave shared global state active?

That sequence solves a surprising number of "mystery" issues quickly.

Read This Next

Clone this wiki locally