From 64c64ad51297c0c6f016f18dc88a83ac57a442b8 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Sun, 4 Oct 2026 13:46:28 -0300 Subject: [PATCH] dynamic_tree: an empty node's bounds are inverted infinities, not the float limits An empty node passes for a leaf (EMPTY_NODE carries the leaf flag), so only its bounds keep a traversal out of it. They were math.aabb_empty(), the float limits (+-MAX_FLOAT) inverted, where the reference's b3MakeEmptyNode uses infinities. A query box reaching the limits -- the natural 'everything' box -- overlapped them: in a tree of one proxy, whose root is a leaf beside an empty node, tree_query took the empty node for a leaf and read proxy 0x3FFFFFFF, an access violation; tree_box_cast had the same path for a cast box grown to the limits. The empty node's bounds are +-infinity now (math.infinity(), new), which no box overlaps. test_dynamic_tree: the leaf root queried with a box out to the float limits finds only its proxy (on main the test crashes). Found by an audit of the dynamic tree and hull creation against Box3D e77352c, otherwise faithful. --- aephysics/dynamic_tree/module.ae | 8 +++++++- aephysics/math/module.ae | 9 ++++++++- tests/test_dynamic_tree.ae | 7 +++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/aephysics/dynamic_tree/module.ae b/aephysics/dynamic_tree/module.ae index ac4589d..9c7b245 100644 --- a/aephysics/dynamic_tree/module.ae +++ b/aephysics/dynamic_tree/module.ae @@ -134,8 +134,14 @@ node_height(n: DynamicTreeNode) -> int { return n.height } +// An empty node passes for a leaf, so only its bounds keep a traversal out: +// inverted infinities, which no box overlaps, as the reference's are. The +// float limits were not enough: a query box out to them overlapped, and the +// empty node's proxy index read far past the proxies. make_empty_node() -> DynamicTreeNode { - return DynamicTreeNode { aabb: math.aabb_empty(), flag_index: EMPTY_NODE, height: 0 } + inf = math.infinity() + return DynamicTreeNode { aabb: AABB { lower: Vec3 { x: inf, y: inf, z: inf }, upper: Vec3 { x: 0.0 - inf, y: 0.0 - inf, z: 0.0 - inf } }, + flag_index: EMPTY_NODE, height: 0 } } // The pair below the root, or the root itself when it is a leaf. diff --git a/aephysics/math/module.ae b/aephysics/math/module.ae index 1c66ff0..4780952 100644 --- a/aephysics/math/module.ae +++ b/aephysics/math/module.ae @@ -37,7 +37,7 @@ exports ( EPSILON, TINY, MAX_FLOAT, HUGE, vec2, vec3, quat, quat_vs, qv, transform, matrix3, aabb, plane, vec3_zero, vec3_one, vec3_axis_x, vec3_axis_y, vec3_axis_z, - quat_identity, transform_identity, mat3_zero, mat3_identity, aabb_empty, + quat_identity, transform_identity, mat3_zero, mat3_identity, aabb_empty, infinity, min_int, max_int, clamp_int, abs_float, min_float, max_float, clamp_float, lerp_float, sqrt_float, atan2_approx, compute_cos_sin, sin_approx, cos_approx, unwind_angle, add, sub, mul, neg, dot, length, length_squared, distance, distance_squared, @@ -182,6 +182,13 @@ transform_identity() -> Transform { return Transform { p: vec3_zero(), q: quat_i mat3_zero() -> Matrix3 { return Matrix3 { cx: vec3_zero(), cy: vec3_zero(), cz: vec3_zero() } } mat3_identity() -> Matrix3 { return Matrix3 { cx: vec3_axis_x(), cy: vec3_axis_y(), cz: vec3_axis_z() } } // The box no point is in: a union with it is the other box. +// IEEE positive infinity: past every finite double, as an empty tree node's +// bounds must be so no finite query box overlaps them (b3MakeEmptyNode). +infinity() -> float { + zero = 0.0 + return 1.0 / zero +} + aabb_empty() -> AABB { return AABB { lower: Vec3 { x: MAX_FLOAT, y: MAX_FLOAT, z: MAX_FLOAT }, upper: Vec3 { x: 0.0 - MAX_FLOAT, y: 0.0 - MAX_FLOAT, z: 0.0 - MAX_FLOAT } } diff --git a/tests/test_dynamic_tree.ae b/tests/test_dynamic_tree.ae index 0d21fca..d0d434d 100644 --- a/tests/test_dynamic_tree.ae +++ b/tests/test_dynamic_tree.ae @@ -160,6 +160,13 @@ test_leaf_root() { ensure("leaf root: the proxy", hit.proxy_id == proxy_id) ensure("leaf root: its user data", hit.user_data == (7 as long)) + // A box out to the float limits: the empty node beside the leaf root is + // still skipped (its bounds are inverted infinities, not the limits). + hit.count = 0 + everything = math.aabb(math.vec3(0.0 - math.MAX_FLOAT, 0.0 - math.MAX_FLOAT, 0.0 - math.MAX_FLOAT), math.vec3(math.MAX_FLOAT, math.MAX_FLOAT, math.MAX_FLOAT)) + dynamic_tree.tree_query(&tree, everything, dynamic_tree.default_mask_bits(), false, leaf_root_visit, (&hit) as ptr) + ensure("leaf root: everything finds only the proxy (${hit.count})", hit.count == 1 && hit.proxy_id == proxy_id) + hit.count = 0 miss_box = math.aabb(math.vec3(4.0, 4.0, 4.0), math.vec3(5.0, 5.0, 5.0)) dynamic_tree.tree_query(&tree, miss_box, dynamic_tree.default_mask_bits(), false, leaf_root_visit, (&hit) as ptr)