Files
libakgl/src/heap.c
Tachikoma 093a1bb59d Add a second partitioner, so the vtable is a seam and not a decoration
A binary space partition on libakstdlib's tree links and lists, registered in
the factory as "bsp". It runs the same assertions the grid does, because
tests/partition.c is a table over partitioners and adding a row is all it takes
to be held to the contract.

**Use the grid.** This is here to prove the vtable works and to have something
to measure the grid against, and the file says so at the top. It rebuilds
whenever the proxy set changes, which is the shape PERFORMANCE.md records Phaser
using and capping out around five thousand bodies. It would earn its place in a
world with wildly non-uniform object sizes, or one larger than the grid's fixed
cell array covers.

The difference is visible in the code rather than buried in a benchmark: the
grid's `move` compares four integers and returns when a proxy has not left its
cells, and this one marks the whole partition stale. The incremental-move
assertion in the suite is therefore grid-only, and says why.

aksl_tree_iterate is not used, and the file carries the three reasons so the
next reader does not rediscover them: it is a complete traversal whose only
control signal stops the entire walk, so there is no way to prune a subtree --
which is the only operation a spatial query is made of; it carries no per-node
context, and a pruning descent needs each node's bounds and plane; and its
breadth-first modes allocate, while only the depth-first ones are malloc-free
and those are the ones without pruning. aksl_tree_insert is unusable for a
different reason again: it is a comparator-ordered BST, and a spatial insert has
to compare a leaf against a plane, which aksl_TreeCompareFunc cannot express.

What is used is the link structure and the lists, neither of which allocates,
which is why they fit here at all. The descent is an explicit stack rather than
recursion, so the depth bound is an array bound the compiler can see -- this
library already has one documented way to blow the C stack and does not need a
second.

Split planes are the median of the item centres on the longer axis, not the
spatial midpoint: actors in a tile game cluster on the floor, and a midpoint
split gives one empty child and one full one for several levels running. A split
that separates nothing degrades the node to a leaf rather than recursing forever
on the same set.

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>
2026-08-02 07:08:54 -04:00

341 lines
10 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];
akgl_BspNode akgl_heap_bspnodes[AKGL_MAX_HEAP_BSPNODE];
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());
PASS(errctx, akgl_heap_init_bspnodes());
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);
}
akerr_ErrorContext *akgl_heap_next_bspnode(akgl_BspNode **dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL destination reference");
for (int i = 0; i < AKGL_MAX_HEAP_BSPNODE; i++ ) {
if ( akgl_heap_bspnodes[i].refcount != 0 ) {
continue;
}
*dest = &akgl_heap_bspnodes[i];
SUCCEED_RETURN(errctx);
}
FAIL_RETURN(errctx, AKGL_ERR_HEAP, "Unable to find unused BSP node on the heap");
}
akerr_ErrorContext *akgl_heap_release_bspnode(akgl_BspNode *ptr)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, ptr, AKERR_NULLPOINTER, "NULL BSP node reference");
if ( ptr->refcount > 0 ) {
ptr->refcount -= 1;
}
if ( ptr->refcount == 0 ) {
memset(ptr, 0x00, sizeof(akgl_BspNode));
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akgl_heap_init_bspnodes(void)
{
PREPARE_ERROR(errctx);
for ( int i = 0; i < AKGL_MAX_HEAP_BSPNODE; i++ ) {
memset(&akgl_heap_bspnodes[i], 0x00, sizeof(akgl_BspNode));
}
SUCCEED_RETURN(errctx);
}