Skip to content

Conversation

@waywardmonkeys
Copy link
Contributor

Hit testing is a hot path and used to pay avoidable per-query overhead.

  • Avoid allocation: use IndexGeneric::visit_point instead of query_point so hit testing does not allocate a Vec of candidates each call.
  • Avoid repeated work: reuse cached world_transform_inverse and depth from Tree::commit while scoring candidates.
  • Preserve behavior: still filters precisely by local_bounds, local_clip, and ancestor clips (after the coarse AABB query), and keeps the existing z/depth/newer tie-break.

This improves hit testing across all index backends because the allocation/inverse/depth work was backend-agnostic overhead.

Benchmark (synthetic ui_box_tree): hit_test_point/flatvec improved about 22% on my machine (~61us -> ~47us per iteration). Command:
cargo bench -p understory_benches --bench ui_box_tree -- ui_box_tree/hit_test_point/flatvec --noplot

Hit testing is a hot path and used to pay avoidable per-query overhead.

- Avoid allocation: use `IndexGeneric::visit_point` instead of `query_point` so hit testing does not
  allocate a `Vec` of candidates each call.
- Avoid repeated work: reuse cached `world_transform_inverse` and `depth` from `Tree::commit` while
  scoring candidates.
- Preserve behavior: still filters precisely by `local_bounds`, `local_clip`, and ancestor clips
  (after the coarse AABB query), and keeps the existing z/depth/newer tie-break.

This improves hit testing across all index backends because the allocation/inverse/depth work was
backend-agnostic overhead.

Benchmark (synthetic `ui_box_tree`): `hit_test_point/flatvec` improved about 22% on my machine
(~61us -> ~47us per iteration). Command:
  `cargo bench -p understory_benches --bench ui_box_tree -- ui_box_tree/hit_test_point/flatvec --noplot`
Copy link
Contributor

@tomcur tomcur left a comment

Choose a reason for hiding this comment

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

Nice speed up! The timings you mentioned reproduce on my machine as well.

I've left some nits inline.

For some workloads we can go further; especially if visit_point yields many nodes from the same subtree for a given point, the repeated tree walking could comparatively get very expensive (as we're traversing the same ancestors over and over towards the root). Fixing that requires either allocating or some mutable scratch context.


if let Some(local_clip) = node.local.local_clip
&& !local_clip.contains(local_point)
// Transform once: most candidates fail at the bounds check, so avoid repeated work.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment doesn't look entirely accurate to me: in the current box tree model, the local bounds are a node's real geometry, and the contains check is the first necessary condition for a hit by finely testing whether the point in local coordinates is within those bounds. The spatial index does that coarsely and it's probably accurate to say the comment as written applies more to the spatial index.

Perhaps instead:

Suggested change
// Transform once: most candidates fail at the bounds check, so avoid repeated work.
// Finely test whether `point` is within the node's bounds and the node's own clip.

if z > *z_best
|| (z == *z_best
None => best = Some((id, z, depth)),
Some((best_id, z_best, depth_best)) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Some((best_id, z_best, depth_best)) => {
Some((id_best, z_best, depth_best)) => {

or

Suggested change
Some((best_id, z_best, depth_best)) => {
Some((best_id, best_z, best_depth)) => {

Comment on lines +503 to +506
// Walk ancestors checking their clips for precise hit filtering.
//
// This is intentionally only done for candidates that pass the local bounds/clip
// checks, since ancestor traversal is comparatively expensive.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is better than the original comment in this place, except that we may want to keep mentioning explicitly this walks towards the root (just so it's immediately clear which direction this is walking in).

Suggested change
// Walk ancestors checking their clips for precise hit filtering.
//
// This is intentionally only done for candidates that pass the local bounds/clip
// checks, since ancestor traversal is comparatively expensive.
// Walk ancestors towards the node's root checking their clips for precise hit filtering.
//
// This is intentionally only done for candidates that pass the local bounds/clip
// checks, since ancestor traversal is comparatively expensive.

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