Pool collision proxies, and tie one to the life of its actor
A proxy is what the broad phase will index: an actor's shape, where it is, and the bounds that follow from those. It is a sixth heap layer, two per actor -- one for the actor itself and headroom for static geometry a game registers that is not tile-aligned. Solid *tiles* deliberately get none: the broad phase will read those out of the tilemap's own array, because one proxy per tile is tens of megabytes to index data that is already a grid. The pool follows the existing convention rather than improving on it. akgl_heap_next_collision_proxy finds a free slot and does not claim it; akgl_collision_proxy_initialize claims it. That is TODO.md item 8's asymmetry, and it is kept on purpose: an acquire abandoned before initialization leaks nothing, because the slot still reads as free, and a sixth pool with its own rule would be worse than one defect with five instances. Fixing item 8 means fixing all five together with every call site audited, and that is its own change. The proxy holds a *copy* of the shape rather than a pointer. An actor's own logic may rewrite its shape mid-step -- a character crouching, a projectile arming -- and a broad phase whose bounds came from a shape that has since changed is a bug that only appears when two things happen in the same frame. akgl_heap_release_actor now releases the actor's proxy. Without it the proxy holds a borrowed `owner` into a slot that has just been zeroed, so the broad phase keeps a registration whose owner reads as a free actor, and the next contact against it is a collision with nothing. Verified by removing the release and watching test_proxy_dies_with_its_actor go red. The tests pin the convention as well as the behaviour: that two acquires with no initialize between them return the same slot is asserted, not merely tolerated, so that changing the convention has to change a test that says why it exists. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -181,3 +181,39 @@ akerr_ErrorContext *akgl_collision_shape_interacts(akgl_CollisionShape *self, ak
|
||||
*dest = ((other->layermask & self->collidemask) != 0);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collision_proxy_initialize(akgl_CollisionProxy *obj, struct akgl_Actor *owner, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL proxy reference");
|
||||
FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");
|
||||
|
||||
memset(obj, 0x00, sizeof(akgl_CollisionProxy));
|
||||
obj->owner = owner;
|
||||
|
||||
/*
|
||||
* The reference is taken here and not in akgl_heap_next_collision_proxy,
|
||||
* matching four of the five pools that came before. Until this runs the slot
|
||||
* still reads as free -- which is also the property that keeps an acquire
|
||||
* abandoned before this point from leaking the slot.
|
||||
*/
|
||||
obj->refcount = 1;
|
||||
|
||||
PASS(errctx, akgl_collision_proxy_sync(obj, shape, x, y, z));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_collision_proxy_sync(akgl_CollisionProxy *obj, akgl_CollisionShape *shape, float32_t x, float32_t y, float32_t z)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "NULL proxy reference");
|
||||
FAIL_ZERO_RETURN(errctx, shape, AKERR_NULLPOINTER, "NULL shape reference");
|
||||
|
||||
// A copy, not a pointer. See the note on akgl_CollisionProxy.
|
||||
obj->shape = *shape;
|
||||
obj->x = x;
|
||||
obj->y = y;
|
||||
obj->z = z;
|
||||
PASS(errctx, akgl_collision_shape_bounds(&obj->shape, x, y, &obj->bounds));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
41
src/heap.c
41
src/heap.c
@@ -19,6 +19,7 @@ akgl_Sprite akgl_heap_sprites[AKGL_MAX_HEAP_SPRITE];
|
||||
akgl_SpriteSheet akgl_heap_spritesheets[AKGL_MAX_HEAP_SPRITESHEET];
|
||||
akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
|
||||
akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
|
||||
akgl_CollisionProxy akgl_heap_collision_proxies[AKGL_MAX_HEAP_COLLISION_PROXY];
|
||||
|
||||
akerr_ErrorContext *akgl_heap_init(void)
|
||||
{
|
||||
@@ -37,6 +38,9 @@ akerr_ErrorContext *akgl_heap_init(void)
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_STRING; i++) {
|
||||
memset(&akgl_heap_strings[i], 0x00, sizeof(akgl_String));
|
||||
}
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_COLLISION_PROXY; i++) {
|
||||
memset(&akgl_heap_collision_proxies[i], 0x00, sizeof(akgl_CollisionProxy));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -129,6 +133,16 @@ akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
||||
PASS(errctx, akgl_heap_release_actor(ptr->children[i]));
|
||||
}
|
||||
}
|
||||
/*
|
||||
* The proxy goes with the actor. It holds a borrowed `owner` pointer
|
||||
* into the slot about to be zeroed, so leaving it registered would give
|
||||
* the broad phase a proxy whose owner reads as a free actor -- and the
|
||||
* next contact against it would report a collision with nothing.
|
||||
*/
|
||||
if ( ptr->proxy != NULL ) {
|
||||
PASS(errctx, akgl_heap_release_collision_proxy(ptr->proxy));
|
||||
ptr->proxy = NULL;
|
||||
}
|
||||
SDL_ClearProperty(AKGL_REGISTRY_ACTOR, (char *)&ptr->name);
|
||||
memset(ptr, 0x00, sizeof(akgl_Actor));
|
||||
}
|
||||
@@ -212,3 +226,30 @@ akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_next_collision_proxy(akgl_CollisionProxy **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination reference");
|
||||
for (int i = 0; i < AKGL_MAX_HEAP_COLLISION_PROXY; i++ ) {
|
||||
if ( akgl_heap_collision_proxies[i].refcount != 0 ) {
|
||||
continue;
|
||||
}
|
||||
*dest = &akgl_heap_collision_proxies[i];
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused collision proxy on the heap");
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_heap_release_collision_proxy(akgl_CollisionProxy *ptr)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL collision proxy reference");
|
||||
if ( ptr->refcount > 0 ) {
|
||||
ptr->refcount -= 1;
|
||||
}
|
||||
if ( ptr->refcount == 0 ) {
|
||||
memset(ptr, 0x00, sizeof(akgl_CollisionProxy));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user