Files
libakgl/include/akgl/heap.h
Andrew Kesterson cf930f68bb 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>
2026-08-02 07:08:54 -04:00

312 lines
15 KiB
C

/**
* @file heap.h
* @brief The object pools. This library does not call `malloc`, it claims slots.
*
* Every runtime object -- actors, sprites, spritesheets, characters, strings --
* comes out of a fixed, statically allocated array declared here. A "heap layer"
* is one such array plus its `next`/`release` pair. Allocation is a linear scan
* for a slot whose `refcount` is 0; release decrements, and the slot is zeroed
* and unregistered when the count reaches 0.
*
* The consequence to design around is that exhaustion is a *normal* error, not
* an out-of-memory catastrophe: AKGL_ERR_HEAP means the pool is full, and the
* fix is usually a missing release rather than a bigger pool. Every ceiling
* below is overridable at compile time, so a game that needs 256 actors defines
* `AKGL_MAX_HEAP_ACTOR` before including this -- but the arrays are sized at
* compile time, so the library and everything linking it must agree.
*
* If you need a new kind of runtime object, add a layer here. Do not reach for
* the allocator.
*
* @warning The acquire functions are asymmetric: akgl_heap_next_string takes the
* reference for you, and the other four do not -- their caller is
* expected to take it, which in practice the `*_initialize` function
* does. Until one is taken the slot is still free and the next
* allocation hands out the same pointer. TODO.md, "Known and still
* open" item 8.
*/
#ifndef _AKGL_HEAP_H_
#define _AKGL_HEAP_H_
#include <akgl/collision.h>
#include <akgl/sprite.h>
#include <akgl/actor.h>
#include <akgl/character.h>
#include <akgl/staticstring.h>
#include <akerror.h>
#ifndef AKGL_MAX_HEAP_ACTOR
#define AKGL_MAX_HEAP_ACTOR 64
#endif
#ifndef AKGL_MAX_HEAP_SPRITE
#define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16)
#endif
#ifndef AKGL_MAX_HEAP_SPRITESHEET
#define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE
#endif
#ifndef AKGL_MAX_HEAP_CHARACTER
#define AKGL_MAX_HEAP_CHARACTER 256
#endif
#ifndef AKGL_MAX_HEAP_STRING
#define AKGL_MAX_HEAP_STRING 256
#endif
/**
* @brief The collision proxy pool.
*
* Two per actor. One is the actor's own; the spare headroom is for static
* geometry a game registers that is not tile-aligned -- a slope, a Tiled object
* rectangle, a moving platform. Solid *tiles* need none of these: the broad
* phase reads them out of the tilemap's own array, because one proxy per tile
* would be tens of megabytes on a large map to index data that is already a grid.
*/
#ifndef AKGL_MAX_HEAP_COLLISION_PROXY
#define AKGL_MAX_HEAP_COLLISION_PROXY (AKGL_MAX_HEAP_ACTOR * 2)
#endif
/**
* @brief The grid cell-entry pool.
*
* One entry per proxy per cell it overlaps. The ceiling is provable rather than
* hopeful: the grid spills anything covering more than
* `AKGL_COLLISION_GRID_MAX_SPAN` cells onto a single chain instead of celling
* it, so no proxy can ever hold more than that many entries.
*/
/** @brief The BSP node pool. A median-split tree over N proxies at 8 per leaf needs about N/2 nodes. */
#ifndef AKGL_MAX_HEAP_BSPNODE
#define AKGL_MAX_HEAP_BSPNODE (AKGL_MAX_HEAP_COLLISION_PROXY)
#endif
#ifndef AKGL_MAX_HEAP_COLLISION_CELL
#define AKGL_MAX_HEAP_COLLISION_CELL (AKGL_MAX_HEAP_COLLISION_PROXY * 16)
#endif
/** @brief The actor pool. Public so the render and physics sweeps can walk it directly instead of going through the registry. */
extern akgl_Actor akgl_heap_actors[AKGL_MAX_HEAP_ACTOR];
/** @brief The sprite pool. 16 per actor, on the assumption of one sprite per state combination. */
extern akgl_Sprite akgl_heap_sprites[AKGL_MAX_HEAP_SPRITE];
/** @brief The spritesheet pool. Sized like the sprite pool, though sharing means far fewer are used in practice. */
extern akgl_SpriteSheet akgl_heap_spritesheets[AKGL_MAX_HEAP_SPRITESHEET];
/** @brief The character pool. */
extern akgl_Character akgl_heap_characters[AKGL_MAX_HEAP_CHARACTER];
/** @brief The string pool. Every entry is PATH_MAX bytes, so this is the largest of the five by a wide margin. */
extern akgl_String akgl_heap_strings[AKGL_MAX_HEAP_STRING];
/** @brief The collision proxy pool. Only the library allocates from it. */
extern akgl_CollisionProxy akgl_heap_collision_proxies[AKGL_MAX_HEAP_COLLISION_PROXY];
/** @brief The grid cell-entry pool. Only the uniform grid allocates from it. */
extern akgl_CollisionCell akgl_heap_collision_cells[AKGL_MAX_HEAP_COLLISION_CELL];
/** @brief The BSP node pool. Only the BSP partitioner allocates from it. */
extern akgl_BspNode akgl_heap_bspnodes[AKGL_MAX_HEAP_BSPNODE];
/**
* @brief Zero every pool, marking every slot free.
*
* Call once at startup, before anything allocates. Calling it again is a reset,
* not a refresh: it does not release textures, clear registries, or consult
* reference counts, so every live object becomes a dangling pointer and every
* registry entry points at a zeroed slot. akgl_game_init calls it for you.
*
* @return `NULL`. There is no failure path today -- it is a series of `memset`s
* -- but check it anyway; the signature exists so a layer that needs
* real setup has somewhere to report from.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init(void);
/**
* @brief Zero the actor pool only.
*
* Split out from akgl_heap_init so a caller can reset the actors between levels
* while keeping the loaded sprites, sheets, and characters -- those are the
* expensive ones, since they own textures. Pair it with
* akgl_registry_init_actor, which clears the matching registry.
*
* @return `NULL`. No failure path today; see akgl_heap_init.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_actor(void);
/**
* @brief Claim a free actor slot.
* @param dest Receives a pointer to the free slot. Required, and **not**
* checked -- a `NULL` here is a crash, not an error context. The
* slot is *not* zeroed and its `refcount` is not incremented; it
* stays free until akgl_actor_initialize takes the reference.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_ACTOR slots is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_actor(akgl_Actor **dest);
/**
* @brief Claim a free sprite slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITE slots is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_sprite(akgl_Sprite **dest);
/**
* @brief Claim a free spritesheet slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_SPRITESHEET slots is
* in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_spritesheet(akgl_SpriteSheet **dest);
/**
* @brief Claim a free character slot.
* @param dest Receives a pointer to the free slot. Required and unchecked; not
* zeroed, and no reference taken. See akgl_heap_next_actor.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_CHARACTER slots is in
* use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_character(akgl_Character **dest);
/**
* @brief Claim a free string slot, and take the reference on it.
*
* The odd one out: this *does* increment `refcount`, so the slot is yours the
* moment it returns and stays yours until you call akgl_heap_release_string.
* That is what makes the scratch-buffer idiom -- claim, use, release in
* `CLEANUP` -- safe. The contents are whatever the last holder left; call
* akgl_string_initialize if you need it clean.
*
* @param dest Receives a pointer to the claimed slot. Required and **not**
* checked -- a `NULL` here is a crash, not an error context.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every one of the #AKGL_MAX_HEAP_STRING slots is in
* use. In practice this means a missing release somewhere, not a pool
* that is genuinely too small.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_string(akgl_String **dest);
/**
* @brief Drop a reference to an actor, tearing it down when the last one goes.
*
* At zero it releases every child recursively, clears the actor's entry from
* #AKGL_REGISTRY_ACTOR, and zeroes the slot.
*
* @param ptr The actor to release. Required. A slot whose `refcount` is already
* 0 is re-torn-down rather than rejected, which is harmless on a
* zeroed slot and destructive on a live one that was never
* registered.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @warning Children are released unconditionally, and the recursion has no cycle
* check: an actor reachable from its own child list recurses until the
* stack runs out.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_actor(akgl_Actor *ptr);
/**
* @brief Drop a reference to a sprite, tearing it down when the last one goes.
*
* At zero it clears the sprite's entry from #AKGL_REGISTRY_SPRITE and zeroes the
* slot. The spritesheet it pointed at is *not* released -- the sprite only
* borrowed it.
*
* @param ptr The sprite to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_sprite(akgl_Sprite *ptr);
/**
* @brief Drop a reference to a spritesheet, destroying its texture when the last one goes.
*
* The only release that frees a resource outside the pool: at zero it clears the
* entry from #AKGL_REGISTRY_SPRITESHEET, destroys the `SDL_Texture`, and zeroes
* the slot. Any sprite still pointing at the sheet is left with a dangling
* pointer, since nothing takes a reference on a sheet on a sprite's behalf.
*
* @param ptr The spritesheet to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @note Destroying a texture is a main-thread operation in SDL, so this must be
* called from the thread that owns the renderer.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_spritesheet(akgl_SpriteSheet *ptr);
/**
* @brief Drop a reference to a character, tearing it down when the last one goes.
*
* At zero it clears the entry from #AKGL_REGISTRY_CHARACTER and zeroes the slot.
*
* @param ptr The character to release. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*
* @note At zero it also walks the state-to-sprite map with
* akgl_character_state_sprites_iterate and #AKGL_ITERATOR_OP_RELEASE,
* giving back every reference akgl_character_sprite_add took, and then
* destroys the `SDL_PropertiesID` holding the map. Before 0.5.0 it did
* neither, so loading and releasing characters level by level exhausted
* the sprite pool and leaked a property set per character.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_character(akgl_Character *ptr);
/**
* @brief Drop a reference to a pooled string, zeroing it when the last one goes.
*
* Strings are not registered anywhere, so this is just the reference count and a
* wipe. It is safe to call on a string a function may or may not have claimed,
* which is why the `CLEANUP` blocks in this library call it unconditionally.
*
* @param ptr The string to release. Required -- unlike the pattern elsewhere, a
* `NULL` here is an error rather than a no-op, so `CLEANUP` blocks
* wrap it in `IGNORE()`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_string(akgl_String *ptr);
/**
* @brief Find a free collision proxy slot.
*
* Does **not** take the reference, matching four of the five pools above.
* akgl_collision_proxy_initialize takes it; write the two adjacent, because
* until it runs the slot still reads as free and the next acquire hands out the
* same pointer.
*
* @param dest Receives the proxy. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_HEAP If every slot is in use. That normally means proxies are
* not being released rather than that the pool is too small.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_collision_proxy(akgl_CollisionProxy **dest);
/**
* @brief Give a collision proxy back.
*
* Decrements, and at zero clears the slot. A proxy holds no external resource
* and appears in no registry, so there is nothing else to unwind.
*
* @param ptr The proxy. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_collision_proxy(akgl_CollisionProxy *ptr);
/**
* @brief Find a free grid cell entry. Does not claim it; the initialize does.
* @param dest Receives the entry. Required.
* @throws AKGL_ERR_HEAP If every slot is in use.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_collision_cell(akgl_CollisionCell **dest);
/**
* @brief Give a grid cell entry back.
* @param ptr The entry. Required.
* @throws AKERR_NULLPOINTER If @p ptr is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_collision_cell(akgl_CollisionCell *ptr);
/**
* @brief Drop every grid cell entry without touching any other pool.
*
* The partitioner's `reset` calls this. Sibling of akgl_heap_init_actor, for the
* same reason: a level change should not cost the sprite and character pools.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_collision_cells(void);
/** @brief Find a free BSP node. Does not claim it. @throws AKGL_ERR_HEAP when full. */
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_next_bspnode(akgl_BspNode **dest);
/** @brief Give a BSP node back. @throws AKERR_NULLPOINTER If @p ptr is `NULL`. */
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_release_bspnode(akgl_BspNode *ptr);
/** @brief Drop every BSP node without touching any other pool. */
akerr_ErrorContext AKERR_NOIGNORE *akgl_heap_init_bspnodes(void);
#endif //_AKGL_HEAP_H_