akgl_Partitioner is a record of function pointers and an initializer, the same shape as the render and physics backends. `move` is its own slot rather than remove-then-insert, and that is the whole design: a uniform grid's move is a comparison and a return when the proxy has not left the cells it was in, which is the steady state for a walking actor and the permanent state for a static one. Spelling it as remove-and-insert would turn the incremental grid into the rebuild-every-frame tree that PERFORMANCE.md already measured and rejected. The grid is a dense 128x128 array of cell heads over the world, cells keyed on tile size, with two intrusive chains per entry -- one through the cell so a query can walk it, one through the proxy so removal unlinks a whole span without searching. Everything is an int16_t index rather than a pointer, so the structure is relocatable and clearing it is a memset. A proxy covering more cells than AKGL_COLLISION_GRID_MAX_SPAN spills onto one chain every query walks, which is what makes the cell pool's ceiling provable rather than hopeful. tests/partition.c compares every query against a linear scan over the same proxies and asserts containment in one direction, not equality. That asymmetry is the contract: over-reporting costs a narrowphase call, under-reporting is a wall an actor walks through. The suite is a table over partitioners so the same assertions run against every implementation, which is what the second one landing later will be held to. Three deliberate breaks, and two of them were not caught the first time: - Removing the min-cell rule so a shared-cell pair reports repeatedly: caught. - Dropping the unlink in `move`: **not caught**, initially. Stale entries do not produce wrong query answers, because the bounds re-check filters them out -- they leak the cell pool until the grid stops working, on a timescale no unit test reaches by accident. Counting live pool entries across 200 moves is the only thing that sees it, and now does: 505 entries against an expected 5. - Swapping floorf for a truncating cast: **not caught, and correctly so.** Everything below zero is clamped to cell 0 either way, so today the two are indistinguishable. The comment claiming truncation is a defect was overstating it and now says what is actually true: the clamp is the only thing hiding it, and a negative world origin or a relaxed clamp would make it real with no test standing in front of it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
303 lines
9.3 KiB
C
303 lines
9.3 KiB
C
/**
|
|
* @file heap.c
|
|
* @brief Implements the heap subsystem.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/game.h>
|
|
#include <akgl/sprite.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/staticstring.h>
|
|
#include <akgl/iterator.h>
|
|
#include <akgl/error.h>
|
|
|
|
akgl_Actor akgl_heap_actors[AKGL_MAX_HEAP_ACTOR];
|
|
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];
|
|
akgl_CollisionCell akgl_heap_collision_cells[AKGL_MAX_HEAP_COLLISION_CELL];
|
|
|
|
akerr_ErrorContext *akgl_heap_init(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
int i = 0;
|
|
PASS(errctx, akgl_heap_init_actor());
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITE; i++) {
|
|
memset(&akgl_heap_sprites[i], 0x00, sizeof(akgl_Sprite));
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++) {
|
|
memset(&akgl_heap_spritesheets[i], 0x00, sizeof(akgl_SpriteSheet));
|
|
}
|
|
for ( i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++) {
|
|
memset(&akgl_heap_characters[i], 0x00, sizeof(akgl_Character));
|
|
}
|
|
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));
|
|
akgl_heap_collision_proxies[i].first = -1;
|
|
}
|
|
PASS(errctx, akgl_heap_init_collision_cells());
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_init_actor(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++) {
|
|
memset(&akgl_heap_actors[i], 0x00, sizeof(akgl_Actor));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_actor(akgl_Actor **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
|
if ( akgl_heap_actors[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_actors[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused actor on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_sprite(akgl_Sprite **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_SPRITE; i++ ) {
|
|
if ( akgl_heap_sprites[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_sprites[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused sprite on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_SPRITESHEET; i++ ) {
|
|
if ( akgl_heap_spritesheets[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_spritesheets[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused spritesheet on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_character(akgl_Character **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_CHARACTER; i++ ) {
|
|
if ( akgl_heap_characters[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_characters[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused character on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_string(akgl_String **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for (int i = 0; i < AKGL_MAX_HEAP_STRING; i++ ) {
|
|
if ( akgl_heap_strings[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_strings[i];
|
|
akgl_heap_strings[i].refcount += 1;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused string on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_actor(akgl_Actor *ptr)
|
|
{
|
|
int i = 0;
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL actor reference");
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
for ( i = 0; i < AKGL_ACTOR_MAX_CHILDREN; i++ ) {
|
|
if ( ptr->children[i] != NULL ) {
|
|
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));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_character(akgl_Character *ptr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_Iterator opflags;
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL character reference");
|
|
AKGL_BITMASK_CLEAR(opflags.flags);
|
|
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
// Give back every sprite reference akgl_character_sprite_add took, and
|
|
// the property set holding the map, before the slot is zeroed. Without
|
|
// this a game that loads and releases characters level by level
|
|
// exhausts the sprite pool and leaks one SDL_PropertiesID per
|
|
// character. akgl_character_state_sprites_iterate exists for exactly
|
|
// this walk and simply was never called from here.
|
|
if ( ptr->state_sprites != 0 ) {
|
|
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_RELEASE);
|
|
SDL_EnumerateProperties(
|
|
ptr->state_sprites,
|
|
&akgl_character_state_sprites_iterate,
|
|
(void *)&opflags
|
|
);
|
|
SDL_DestroyProperties(ptr->state_sprites);
|
|
ptr->state_sprites = 0;
|
|
}
|
|
SDL_ClearProperty(AKGL_REGISTRY_CHARACTER, (char *)&ptr->name);
|
|
memset(ptr, 0x00, sizeof(akgl_Character));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_sprite(akgl_Sprite *ptr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL sprite reference");
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
SDL_ClearProperty(AKGL_REGISTRY_SPRITE, (char *)&ptr->name);
|
|
memset(ptr, 0x00, sizeof(akgl_Sprite));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL spritesheet reference");
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
// TODO : If we go threaded, make sure this is only happening on the main thread
|
|
SDL_ClearProperty(AKGL_REGISTRY_SPRITESHEET, (char *)&ptr->name);
|
|
if ( ptr-> texture != NULL )
|
|
SDL_DestroyTexture(ptr->texture);
|
|
ptr->texture = NULL;
|
|
memset(ptr, 0x00, sizeof(akgl_SpriteSheet));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_string(akgl_String *ptr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "Received NULL string reference");
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
memset(&ptr->data, 0x00, AKGL_MAX_STRING_LENGTH);
|
|
}
|
|
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);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_next_collision_cell(akgl_CollisionCell **dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination reference");
|
|
for (int i = 0; i < AKGL_MAX_HEAP_COLLISION_CELL; i++ ) {
|
|
if ( akgl_heap_collision_cells[i].refcount != 0 ) {
|
|
continue;
|
|
}
|
|
*dest = &akgl_heap_collision_cells[i];
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused collision cell entry on the heap");
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_release_collision_cell(akgl_CollisionCell *ptr)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL collision cell reference");
|
|
if ( ptr->refcount > 0 ) {
|
|
ptr->refcount -= 1;
|
|
}
|
|
if ( ptr->refcount == 0 ) {
|
|
memset(ptr, 0x00, sizeof(akgl_CollisionCell));
|
|
ptr->next = -1;
|
|
ptr->prev = -1;
|
|
ptr->ownernext = -1;
|
|
ptr->cell = -1;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_heap_init_collision_cells(void)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
for ( int i = 0; i < AKGL_MAX_HEAP_COLLISION_CELL; i++ ) {
|
|
memset(&akgl_heap_collision_cells[i], 0x00, sizeof(akgl_CollisionCell));
|
|
akgl_heap_collision_cells[i].next = -1;
|
|
akgl_heap_collision_cells[i].prev = -1;
|
|
akgl_heap_collision_cells[i].ownernext = -1;
|
|
akgl_heap_collision_cells[i].cell = -1;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|