TODO.md carried two records in one file: what had been done, with the measurements behind it, and what was left. The second half is what a tracker is for, and keeping it here has already cost something -- AGENTS.md records a round where eleven entries described code that had already changed, and this file admitted to three more. Every open item is now an issue on source.starfort.tech/andrew/libakgl, labelled by kind and blast radius and milestoned by what it can land in: 0.9.x for anything that breaks no ABI, 0.10.0 for new or changed public symbols, 1.0.0 for the design work. Four are epics: the performance plan (#60), coverage (#61), actor rotation (#62), and the false header comments (#63). Verified against the tree before filing rather than transcribed. Three entries were already fixed and were not filed: the akgl_path_relative context leak, the akgl_draw_background test extension, and the SDL enumeration audit -- keyboards, gamepads and mappings are all freed in CLEANUP today. Two were reworded because the code had moved: the fonts item is a missing teardown entry point rather than a missing API, since akgl_text_unloadallfonts exists, and draw_world's tilemap call is already bounded by numlayers, so only the per-layer actor rescan remains. TODO.md keeps the part a tracker has no place for: why a decision went the way it did, what the measurement was, and which arguments turned out to be wrong. TODO.txt is deleted. Four of its eight entries had shipped -- actor-to-actor collision, actor-to-world collision, automatic facing, image layers -- and the four that had not are #74 through #77, with the GPU renderer's research links kept because that is the part that took the time. Every reference that named an item number or a moved section is repointed, in the manual, the headers, the tests and the examples. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
312 lines
15 KiB
C
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. Whether to make them symmetric
|
|
* or document the split is issue #14.
|
|
*/
|
|
|
|
#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_
|