From 46e937050bafc881229f0045a9cd730e12f027c7 Mon Sep 17 00:00:00 2001 From: Nicolas Maman Date: Mon, 19 Oct 2026 13:45:48 -0300 Subject: [PATCH] World queries borrow their context instead of keeping it (#179) cast_ray, cast_shape, cast_mover, overlap_aabb, overlap_shape and collide_mover declared context: @keep ptr only because the helper query_recorder took the context and returned it inside a QueryRecorder. None of them keeps the pointer: each lends its local recorder to the internal query for the call. The helper now builds the recorder without a context, and each query sets q.context on its own local, which Aether 0.830's narrowed E0902 accepts. Callers can pass a borrowed pointer again, so Siltreach's blade ray no longer needs @keep climbing four functions. The general form, a parameter that flows into a result without being kept, is aether#2990. test_physics_world forwards a plain borrowed ptr through all six queries; with @keep back on any of them the file fails to build. --- aephysics/physics_world/module.ae | 37 +++++++++++++++++++------------ tests/test_physics_world.ae | 18 +++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/aephysics/physics_world/module.ae b/aephysics/physics_world/module.ae index a7455a5..5b40d3d 100644 --- a/aephysics/physics_world/module.ae +++ b/aephysics/physics_world/module.ae @@ -1096,9 +1096,12 @@ accept_shape(shape_id: ShapeId, context: ptr) -> bool { return true } keep_cast(shape_id: ShapeId, point: Vec3, normal: Vec3, fraction: float, user_material_id: long, triangle_index: int, child_index: int, context: ptr) -> float { return 1.0 } accept_planes(shape_id: ShapeId, planes: PlaneResult[], count: int, context: ptr) -> bool { return true } -query_recorder(context: @keep ptr) -> QueryRecorder { +// A recorder with no visitor and no context yet: each query sets the +// caller's on its own local, which it lends to the internal query only, +// so the caller's context is borrowed, never kept (#179). +query_recorder() -> QueryRecorder { return QueryRecorder { payload: dynamics.rec_payload_create(), overlap: accept_shape, cast: keep_cast, planes: accept_planes, - has_overlap: false, context: context, count: 0 } + has_overlap: false, context: null, count: 0 } } // Whether the query asked of `id` is one to record: the world records and @@ -1166,9 +1169,10 @@ record_plane_visit(shape_id: ShapeId, planes: PlaneResult[], count: int, context // Every shape whose fat bounds overlap the box, through the visitor // until it returns false; the tree visits made. -overlap_aabb(id: WorldId, box: AABB, filter: QueryFilter, visitor: fn(ShapeId, ptr) -> bool, context: @keep ptr) -> TreeStats { +overlap_aabb(id: WorldId, box: AABB, filter: QueryFilter, visitor: fn(ShapeId, ptr) -> bool, context: ptr) -> TreeStats { if recording_query(id) == false { return overlap_aabb_internal(id, box, filter, visitor, context) } - q = query_recorder(context) + q = query_recorder() + q.context = context q.overlap = visitor q.has_overlap = true dynamics.rec_aabb(q.payload, box) @@ -1183,9 +1187,10 @@ overlap_aabb(id: WorldId, box: AABB, filter: QueryFilter, visitor: fn(ShapeId, p } // Every shape a convex proxy (given relative to the origin) overlaps. -overlap_shape(id: WorldId, origin: Vec3, proxy: ShapeProxy, filter: QueryFilter, visitor: fn(ShapeId, ptr) -> bool, context: @keep ptr) -> TreeStats { +overlap_shape(id: WorldId, origin: Vec3, proxy: ShapeProxy, filter: QueryFilter, visitor: fn(ShapeId, ptr) -> bool, context: ptr) -> TreeStats { if recording_query(id) == false { return overlap_shape_internal(id, origin, proxy, filter, visitor, context) } - q = query_recorder(context) + q = query_recorder() + q.context = context q.overlap = visitor q.has_overlap = true dynamics.rec_vec3(q.payload, origin) @@ -1203,12 +1208,13 @@ overlap_shape(id: WorldId, origin: Vec3, proxy: ShapeProxy, filter: QueryFilter, // The planes of every shape the character mover's capsule (relative to // the origin) touches, a shape's planes at a time (up to 64), through // the visitor until it returns false. -collide_mover(id: WorldId, origin: Vec3, mover: Capsule, filter: QueryFilter, visitor: fn(ShapeId, PlaneResult[], int, ptr) -> bool, context: @keep ptr) { +collide_mover(id: WorldId, origin: Vec3, mover: Capsule, filter: QueryFilter, visitor: fn(ShapeId, PlaneResult[], int, ptr) -> bool, context: ptr) { if recording_query(id) == false { collide_mover_internal(id, origin, mover, filter, visitor, context) return } - q = query_recorder(context) + q = query_recorder() + q.context = context q.planes = visitor dynamics.rec_vec3(q.payload, origin) dynamics.rec_capsule_value(q.payload, mover) @@ -1224,9 +1230,10 @@ collide_mover(id: WorldId, origin: Vec3, mover: Capsule, filter: QueryFilter, vi // visitor, whose return clips the ray (0 stops it, 1 keeps everything, // -1 ignores that shape); the tree visits made. cast_ray(id: WorldId, origin: Vec3, translation: Vec3, filter: QueryFilter, - visitor: fn(ShapeId, Vec3, Vec3, float, long, int, int, ptr) -> float, context: @keep ptr) -> TreeStats { + visitor: fn(ShapeId, Vec3, Vec3, float, long, int, int, ptr) -> float, context: ptr) -> TreeStats { if recording_query(id) == false { return cast_ray_internal(id, origin, translation, filter, visitor, context) } - q = query_recorder(context) + q = query_recorder() + q.context = context q.cast = visitor dynamics.rec_vec3(q.payload, origin) dynamics.rec_vec3(q.payload, translation) @@ -1270,9 +1277,10 @@ rec_ray_result(payload: *Recording, result: RayResult) { // A convex proxy (relative to the origin) swept along the translation, // each hit through the visitor as the ray cast's. cast_shape(id: WorldId, origin: Vec3, proxy: ShapeProxy, translation: Vec3, filter: QueryFilter, - visitor: fn(ShapeId, Vec3, Vec3, float, long, int, int, ptr) -> float, context: @keep ptr) -> TreeStats { + visitor: fn(ShapeId, Vec3, Vec3, float, long, int, int, ptr) -> float, context: ptr) -> TreeStats { if recording_query(id) == false { return cast_shape_internal(id, origin, proxy, translation, filter, visitor, context) } - q = query_recorder(context) + q = query_recorder() + q.context = context q.cast = visitor dynamics.rec_vec3(q.payload, origin) dynamics.rec_shape_proxy(q.payload, proxy) @@ -1291,9 +1299,10 @@ cast_shape(id: WorldId, origin: Vec3, proxy: ShapeProxy, translation: Vec3, filt // touching a shape (its filter asked per shape when given). Recorded, the // filter is asked through the recorder for every candidate, given or not, // so a replay sees the same decisions. -cast_mover(id: WorldId, origin: Vec3, mover: Capsule, translation: Vec3, filter: QueryFilter, has_filter: bool, filter_fn: fn(ShapeId, ptr) -> bool, context: @keep ptr) -> float { +cast_mover(id: WorldId, origin: Vec3, mover: Capsule, translation: Vec3, filter: QueryFilter, has_filter: bool, filter_fn: fn(ShapeId, ptr) -> bool, context: ptr) -> float { if recording_query(id) == false { return cast_mover_internal(id, origin, mover, translation, filter, has_filter, filter_fn, context) } - q = query_recorder(context) + q = query_recorder() + q.context = context q.overlap = filter_fn q.has_overlap = has_filter dynamics.rec_vec3(q.payload, origin) diff --git a/tests/test_physics_world.ae b/tests/test_physics_world.ae index cae2531..5c445b5 100644 --- a/tests/test_physics_world.ae +++ b/tests/test_physics_world.ae @@ -2057,6 +2057,19 @@ count_planes(id: ShapeId, planes: PlaneResult[], count: int, context: ptr) -> bo return true } +// #179: a caller's borrowed context reaches every world query without +// @keep, since a query only lends it to its own recorder for the call. +// The context here is a plain ptr on purpose: if a query required @keep +// again, this file would stop building. The queries count into it. +borrowed_queries(world_id: WorldId, filter: QueryFilter, context: ptr) { + physics_world.cast_ray(world_id, math.vec3_zero(), math.vec3(20.0, 0.0, 0.0), filter, count_and_clip, context) + physics_world.overlap_aabb(world_id, AABB { lower: math.vec3(3.0, 0.0 - 1.0, 0.0 - 1.0), upper: math.vec3(11.5, 1.0, 1.0) }, filter, count_overlap, context) + physics_world.overlap_shape(world_id, math.vec3(10.0, 0.0, 0.0), point_proxy(math.vec3_zero(), 1.5), filter, count_overlap, context) + physics_world.cast_shape(world_id, math.vec3_zero(), point_proxy(math.vec3_zero(), 0.25), math.vec3(20.0, 0.0, 0.0), filter, count_and_clip, context) + physics_world.cast_mover(world_id, math.vec3_zero(), Capsule { center1: math.vec3_zero(), center2: math.vec3(0.0, 1.0, 0.0), radius: 0.25 }, math.vec3(10.0, 0.0, 0.0), filter, true, count_overlap, context) + physics_world.collide_mover(world_id, math.vec3(4.0, 0.0, 0.0), Capsule { center1: math.vec3(0.0, 0.0 - 0.5, 0.0), center2: math.vec3(0.0, 0.5, 0.0), radius: 0.3 }, filter, count_planes, context) +} + test_world_queries() { wdef = dynamics.default_world_def() world_id = dynamics.create_world(&wdef) @@ -2097,6 +2110,11 @@ test_world_queries() { counter[0] = 0 physics_world.overlap_shape(world_id, math.vec3(10.0, 5.0, 0.0), point_proxy(math.vec3_zero(), 1.5), filter, count_overlap, counter_block) ensure("world overlap shape: none above", counter[0] == 0) + // A borrowed context (no @keep) through all six queries: the ray's 3, + // the box's 2, the proxy's 1 and the cast's 3, plus the mover's (#179). + counter[0] = 0 + borrowed_queries(world_id, filter, counter_block) + ensure("world queries: a borrowed context reaches all six (${counter[0]})", counter[0] >= 9) // A shape cast along the row: three hits when nothing clips. counter[0] = 0 physics_world.cast_shape(world_id, math.vec3_zero(), point_proxy(math.vec3_zero(), 0.25), math.vec3(20.0, 0.0, 0.0), filter, count_and_clip, counter_block)