Skip to content

Conversation

@tomcur
Copy link
Contributor

@tomcur tomcur commented Dec 29, 2025

If the number of cells covered by the rect is larger than the number of cells actually occupied in the grid, just iterate over all occupied grids.

Perhaps if
https://doc.rust-lang.org/stable/std/collections/struct.BTreeMap.html#method.lower_bound gets stabilized, using a BTreeMap as the internal cell representation could be considered.

In the meantime, this prevents runtime from exploding if the input rect is huge.

If the number of cells covered by the `rect` is larger than the number
of cells actually occupied in the grid, just iterate over all occupied
grids.

Perhaps if
`https://doc.rust-lang.org/stable/std/collections/struct.BTreeMap.html#method.lower_bound`
gets stabilized, using a `BTreeMap` as the internal cell representation
could be considered.

In the meantime, this prevents runtime from exploding if the input
`rect` is huge.
@waywardmonkeys
Copy link
Contributor

Codex points out that this still would have an issue upon insertion / update of large objects and offers up a suggestion:

The core issue is that a uniform grid is great when most items are “small” relative to the cell size. It falls apart when an item spans a huge number of cells, because the grid tries to represent that item by duplicating its slot index into every covered cell.

That hurts in two places:

  1. Insert/update cost
  • Today covered_cells(aabb) enumerates every (ix, iy) in the cell-range and stores them in
    SlotEntry.cells.
  • For a huge AABB, that’s O(#cells covered) time and memory, and can explode.
  1. Query cost
  • Even with the visit_rect fallback, if huge items are stored by duplicating into many cells, you still
    pay to walk those duplicates.

“Oversized slots” idea (common pattern)

Keep the grid for the “normal” objects, but route “oversized” objects into a separate list that is
scanned linearly during queries.

Data structure shape

  • Existing:
    • cells: HashMap<(i32,i32), Cell { slots: Vec<usize> }>
    • slots: Vec<Option<SlotEntry { aabb, cells: SmallVec<[(i32,i32);..]> }>>
  • Add:
    • large_slots: Vec<usize> (or HashSet<usize>), containing slot indices whose AABB spans “too many”
      cells.

Policy: decide what “too many” means
Compute cell_span like in grid-fallback:

  • span_x = (ix1 - ix0 + 1)
  • span_y = (iy1 - iy0 + 1)
  • cell_span = span_x * span_y (saturating)

Then choose a threshold. Options:

  • Absolute: cell_span > 1024 (or some constant tuned by bench)
  • Relative: cell_span > self.cells.len() (like the visit fallback)
  • Hybrid: cell_span > max(ABS, RELATIVE).

Insert/update logic

  • When inserting/updating a slot:
    • Compute cell_span for its AABB.
    • If “large”:
      • Don’t compute/store cells at all.
      • Put it in large_slots.
      • Store SlotEntry { aabb, cells: empty } (or a flag is_large).
    • Else (normal):
      • Do what you do today: compute covered_cells, insert into cells, store cells list.

Remove logic

  • If the entry is large:
    • remove from large_slots (you’ll want O(1) removal; common trick is keep a large_pos: Option<usize>
      in SlotEntry to swap-remove).
  • Else:
    • existing cell-removal path.

Query logic
For visit_point:

  • Current grid lookup finds candidates in one cell.
  • Additionally check all large_slots:
    • for each slot in large_slots, test entry.aabb.contains_point(x,y) and emit.

For visit_rect:

  • Use current grid logic (including the grid-fallback heuristic if you keep it).
  • Additionally scan large_slots and test entry.aabb.overlaps(&rect) and emit (with seen to dedupe).

Why this is a win

  • It bounds worst-case memory/time for inserts/updates of huge items.
  • It bounds worst-case query time caused by huge items being duplicated across many cells.
  • It keeps the fast path fast for the common case (small items).

Tradeoffs / questions you have to answer

  • Threshold choice (and whether it depends on current occupancy).
  • How to represent “large” in SlotEntry so updates can move between normal/large efficiently.
  • Whether large_slots should be one list, or multiple tiers (e.g. “medium large” vs “very large”)—
    probably overkill.

@waywardmonkeys
Copy link
Contributor

I questioned it about this being a "common trick" and it pointed at:

  • Christer Ericson, Real-Time Collision Detection (Morgan Kaufmann, 2005): the sections on uniform
    grids / spatial hashing discuss the core tradeoff (objects spanning many cells) and the standard
    mitigation of treating those as “special” (i.e. not inserting into all cells / handling separately).
  • Game Programming Gems series (various volumes/articles) on spatial hashing and uniform grid broadphase:
    a recurring practical note is “large objects kill hashing performance; handle them separately (or use a
    different broadphase)”.
  • Broadphase implementations in physics/game engines often do the same thing in spirit even when not
    literally a grid: they have a “fast structure” plus a “fallback set” that’s tested more directly when
    something doesn’t fit the fast structure well. (E.g. “MBP / pruning structures” often keep certain
    actors in a different bucket or global list.)

but RTCD doesn't actually suggest this (well, not exactly). It suggests a hierarchical grid (one can argue that the simplest 2 level grid is one where the one for big objects is a list of them...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants