Closes internal-consistency items 1 through 6, 12 and 13. Every include guard is _AKGL_<FILE>_H_, every in-project header include is angled, and every exported function, type and global carries the akgl_ prefix. This is an ABI break; the soname goes to libakgl.so.0.5. TODO.md carries the full rename table. The renames were driven by renaming each declaration and letting the compiler find the uses, not by pattern substitution: renderer, physics and camera are also parameter and struct-member names, and a sed would have rewritten map->physics and every akgl_RenderBackend *renderer parameter without a word. Item 4 turned out not to be cosmetic. The library exported a global called renderer and tests/character.c defined an SDL_Renderer *renderer of its own; the executable's definition preempted the library's, akgl_sprite_load_json read a SDL_Renderer * through an akgl_RenderBackend *, and every texture load in that suite failed. The suite reported success anyway, because libakerror's unhandled-error handler ends in exit(errctx->status), exit keeps only the low byte, and AKGL_ERR_SDL is exactly 256. So character had been green while running one of its four tests, and every suite in the tree was unable to fail on the most common status in a library built on SDL. Both are fixed. tests/testutil.h gains TEST_TRAP_UNHANDLED_ERRORS(), which collapses any status a byte cannot carry onto 1, and every suite installs it. character binds a real backend with akgl_render_2d_bind. Its fourth test then runs for the first time and fails on a defect it has asserted all along, so akgl_heap_release_character now walks state_sprites with AKGL_ITERATOR_OP_RELEASE and destroys the property set before zeroing the slot -- TODO.md Defects item 21 and half of Carried over item 1. AKGL_TIME_ONESEC_MS said "one second in milliseconds" and held 1000000, so akgl_game_state_lock waited roughly sixteen minutes rather than one second. It is AKGL_TIME_ONEMS_NS now, the budget is its own named constant, and tests/game.c holds the mutex from a second thread to assert the wait -- the contended path had no coverage at all. Headers are self-contained and it is enforced: AKGL_PUBLIC_HEADERS drives both install() and a generated translation unit per header, so a header that ships is a header that is checked. Writing that found registry.h, which used SDL_PropertiesID in eight declarations and included no SDL header. 23/23 suites pass, memcheck is clean, reindent --check is clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
222 lines
10 KiB
C
222 lines
10 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/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 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 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();
|
|
/**
|
|
* @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();
|
|
/**
|
|
* @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);
|
|
|
|
#endif //_AKGL_HEAP_H_
|