Skip to content

feat(atlas): implement spatial hash grid for O(1) hit-testing#20

Open
Briany4717 wants to merge 6 commits into
mainfrom
feat/atlas-spatial-grid
Open

feat(atlas): implement spatial hash grid for O(1) hit-testing#20
Briany4717 wants to merge 6 commits into
mainfrom
feat/atlas-spatial-grid

Conversation

@Briany4717

Copy link
Copy Markdown
Owner

What does this PR do?

Implements the spatial hash grid described in RFC-0001 §4.1, the second
data structure owned by the Atlas subsystem alongside the Taffy layout
tree.

SpatialGrid maps screen coordinates to TargetIds for pointer event
resolution (clicks, hovers, drags) in O(1) amortised time, without
walking the UI tree.

Implementation highlights:

  • Key packing: cell coordinates (row: i32, col: i32) are packed
    into a single u64 via bit-reinterpretation, so the hash function
    operates on a single word — no tuple overhead.
  • FxHash (rustc-hash): replaces the default SipHash for ~3-5×
    faster hashing on small keys. Appropriate for non-adversarial input.
  • SmallVec<[GridEntry; 4]> (smallvec): inline storage for up to
    4 entries per cell. In typical UI workloads (1-3 entries per 128×128
    cell), this eliminates all heap allocation for bucket storage.
  • Negative coordinate support: quantize uses .floor() rather
    than as i32 truncation, so rects partially off-screen produce the
    correct cell indices.
  • Large rect handling: rects spanning multiple cells are inserted
    into every cell their AABB touches (industry standard for spatial
    hash grids). For typical UI elements this means 1-9 cells per rect.

The grid is a standalone struct — integration with LayoutAtlas will
land in a separate PR that wires populate_frame into the grid.

Linked issue

Closes #17

Acceptance criteria

  • 1000 indexed rects, query answered in < 1 µs.
    Measured at 5.3 ns per query — 188× faster than the target.
    Query time is constant regardless of grid size (100, 1000, or 10000
    rects all measure ~5 ns).
  • Grid handles overlapping rects correctly (returns topmost).
    Verified by overlapping_rects_return_last_inserted. The Z-order
    contract is: later insertion wins, matching the Encoder's dispatch
    order (documented in module-level docs).
  • Recomputing layout updates the grid without leaking stale entries.
    Verified by clear_and_reinsert_does_not_leak_stale_entries and
    repeated_clear_reinsert_cycles_stay_correct, which simulate
    multiple frame cycles and assert no ghost entries survive.

Performance

Benchmarks on release build:

Query (the hot path — one lookup per pointer event)

Rects in grid Hit Miss
100 5.6 ns 1.9 ns
1 000 5.3 ns 2.0 ns
10 000 5.3 ns 1.8 ns

Query time does not grow with grid size — textbook O(1). Miss is
3× faster than hit because it short-circuits at the HashMap lookup
without iterating the SmallVec or calling Rect::contains.

Insert (one-time cost per frame rebuild)

Rects Total Per rect
100 8.8 µs 88 ns
1 000 50 µs 50 ns
10 000 662 µs 66 ns

For a 1000-element UI, populating the grid costs 50 µs = 0.3% of the
60 FPS budget. Combined with the layout recompute (115 µs for the
same size), the total Atlas cost per frame is ~165 µs = 1.0%.

Design decisions

  1. Cell size = 128 px. Power-of-two, balances per-cell occupancy vs
    cells-per-rect for typical UI elements (buttons, labels, inputs).
    Documented as a constant; configurable in a future sub-issue if
    benchmarks on real UIs demand it.

  2. FxHash over SipHash. The grid keys are non-adversarial (derived
    from layout coordinates), so cryptographic resistance is unnecessary.
    FxHash is ~3-5× faster for u64 keys.

  3. SmallVec<[GridEntry; 4]>. 1-3 entries per cell is the common
    case. Inline storage eliminates heap allocation for the dominant
    workload.

  4. Multi-cell insertion for large rects. Industry standard;
    simpler and faster than maintaining a separate "large objects" list.
    Memory cost is linear in cells touched, bounded by ~9 for typical
    UI containers.

  5. Z-order via insertion order. "Last inserted wins" matches the
    pre-order tree traversal of populate_frame. Documented as a
    module-level contract that callers (and the future Encoder) must
    respect.

Checklist

  • cargo fmt --all passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes (+16 spatial grid tests)
  • New public items have doc comments
  • If this changes behavior visible to users, CHANGELOG.md has an entry under [Unreleased]
  • If this changes the architecture, an RFC is linked or opened alongside this PR

Notes for reviewers

GridEntry is pub-in-module only (not re-exported). External code
interacts with the grid via insert(Rect, TargetId) and
query(f32, f32) -> Option<TargetId>.

The #[allow(clippy::cast_sign_loss)] on pack_key is intentional:
the i32 → u32 cast is a bit-level reinterpretation that preserves
the bit pattern, ensuring negative coordinates produce distinct keys.
The test pack_key_distinguishes_signed_coordinates covers this.

CHANGELOG checkbox N/A — pre-alpha.

Copilot AI review requested due to automatic review settings June 1, 2026 17:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the Atlas-owned spatial hash grid described in RFC-0001 §4.1 to enable amortized O(1) pointer hit-testing by mapping screen-space cells to TargetIds, with accompanying tests and a micro-benchmark.

Changes:

  • Added SpatialGrid implementation (FxHashMap + SmallVec buckets) with insertion, query, and extensive correctness tests.
  • Exposed the spatial module and re-exported SpatialGrid / CELL_SIZE from atlas.
  • Added dependencies (rustc-hash, smallvec) and a new cargo bench --bench spatial benchmark target.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
crates/byard-core/src/atlas/spatial.rs New SpatialGrid implementation + tests for hit-testing and lifecycle behavior.
crates/byard-core/src/atlas/mod.rs Exposes spatial module and re-exports SpatialGrid/CELL_SIZE.
crates/byard-core/Cargo.toml Adds rustc-hash + smallvec deps and registers the spatial bench.
crates/byard-core/benches/spatial.rs Adds micro-benchmarks for insert and query hit/miss performance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/byard-core/src/atlas/spatial.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread Cargo.toml
Comment on lines 18 to +19
authors = ["Byard contributors"]
rust-version = "1.85"
rust-version = "1.86"
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.

feat(atlas): implement spatial hash grid for O(1) hit-testing

2 participants