/** * @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 #include #include #include #include #include #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. */ #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 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); #endif //_AKGL_HEAP_H_