feat(atlas): implement spatial hash grid for O(1) hit-testing#20
Open
Briany4717 wants to merge 6 commits into
Open
feat(atlas): implement spatial hash grid for O(1) hit-testing#20Briany4717 wants to merge 6 commits into
Briany4717 wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
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
SpatialGridimplementation (FxHashMap + SmallVec buckets) with insertion, query, and extensive correctness tests. - Exposed the spatial module and re-exported
SpatialGrid/CELL_SIZEfromatlas. - Added dependencies (
rustc-hash,smallvec) and a newcargo bench --bench spatialbenchmark 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 on lines
18
to
+19
| authors = ["Byard contributors"] | ||
| rust-version = "1.85" | ||
| rust-version = "1.86" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
SpatialGridmaps screen coordinates toTargetIds for pointer eventresolution (clicks, hovers, drags) in O(1) amortised time, without
walking the UI tree.
Implementation highlights:
(row: i32, col: i32)are packedinto a single
u64via bit-reinterpretation, so the hash functionoperates on a single word — no tuple overhead.
rustc-hash): replaces the default SipHash for ~3-5×faster hashing on small keys. Appropriate for non-adversarial input.
smallvec): inline storage for up to4 entries per cell. In typical UI workloads (1-3 entries per 128×128
cell), this eliminates all heap allocation for bucket storage.
quantizeuses.floor()ratherthan
as i32truncation, so rects partially off-screen produce thecorrect cell indices.
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
LayoutAtlaswillland in a separate PR that wires
populate_frameinto the grid.Linked issue
Closes #17
Acceptance criteria
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).
Verified by
overlapping_rects_return_last_inserted. The Z-ordercontract is: later insertion wins, matching the Encoder's dispatch
order (documented in module-level docs).
Verified by
clear_and_reinsert_does_not_leak_stale_entriesandrepeated_clear_reinsert_cycles_stay_correct, which simulatemultiple frame cycles and assert no ghost entries survive.
Performance
Benchmarks on release build:
Query (the hot path — one lookup per pointer event)
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)
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
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.
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.
SmallVec<[GridEntry; 4]>. 1-3 entries per cell is the common
case. Inline storage eliminates heap allocation for the dominant
workload.
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.
Z-order via insertion order. "Last inserted wins" matches the
pre-order tree traversal of
populate_frame. Documented as amodule-level contract that callers (and the future Encoder) must
respect.
Checklist
cargo fmt --allpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (+16 spatial grid tests)CHANGELOG.mdhas an entry under[Unreleased]Notes for reviewers
GridEntryispub-in-module only (not re-exported). External codeinteracts with the grid via
insert(Rect, TargetId)andquery(f32, f32) -> Option<TargetId>.The
#[allow(clippy::cast_sign_loss)]onpack_keyis intentional:the
i32 → u32cast is a bit-level reinterpretation that preservesthe bit pattern, ensuring negative coordinates produce distinct keys.
The test
pack_key_distinguishes_signed_coordinatescovers this.CHANGELOG checkbox N/A — pre-alpha.