Give back every pooled string and sprite reference the loaders take

Closes Defects items 20, 22, 23 and 29, and the residual of item 38.

The tilemap load leak was five pooled strings per load; the property-lookup
fix in an earlier commit took it to two, and the last two were each a claim
with no matching release -- the string every layer's `type` was read into, and
the dirname the map's relative paths resolve against. tests/tilemap.c asserts
the pool is exactly where it started after one load/release cycle and after 64.

Finding those two was a matter of dumping the contents of every still-claimed
slot after a cycle rather than reading the code again; 'tilelayer' and an
assets directory named themselves immediately.

Same file, same class, fixed with it: akgl_tilemap_load_layer_objects released
its scratch string after reading each object's name and then kept using the
slot, because akgl_get_json_string_value reuses a non-NULL destination without
taking another reference. The slot was free while still live, so any other
claim could have been handed it.

akgl_character_sprite_add wrote over an existing binding without releasing the
sprite it displaced, so a character that rebinds a state while alive leaked a
sprite slot per rebind -- teardown only gives back what the map holds at the
end. The new reference is taken before the write and given back if the write
fails, so there is no window where a sprite is bound with nothing behind it.
The write was unchecked too.

Three failure-path leaks moved into CLEANUP blocks: akgl_render_2d_init's two
pooled strings, akgl_controller_open_gamepads' enumeration array, and
akgl_text_rendertextat's surface and texture -- the last being a leak per frame
on a HUD line.

akgl_text_unloadallfonts() closes every font in the registry and destroys it,
which is what item 38 left open. Deliberately not a whole akgl_game_shutdown:
tearing down the mixer, SDL_ttf and SDL in the right order is a design
question, and this is the part that was simply missing. It is a new public
symbol, which 0.5.0 already covers -- this release has not shipped.

Every fix has a test that fails against the old code.

25/25 pass, memcheck clean, reindent --check, check_api_surface and
check_error_protocol all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:44:50 -04:00
parent 8a90cbf275
commit 9c2f80bcb9
11 changed files with 475 additions and 94 deletions

View File

@@ -69,6 +69,24 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_text_loadfont(char *name, char *filepath
* left with a dangling pointer. Fonts are not reference counted.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadfont(char *name);
/**
* @brief Close every font in #AKGL_REGISTRY_FONT and destroy the registry.
*
* A font, once loaded, was reachable only by name -- so a game that exited
* without unloading each one by hand left them open, and `SDL_Quit` destroying
* the property registry took the last reference to them with it. That is
* bounded by how many fonts a game loads rather than unbounded, but there was
* no way to do anything about it at all.
*
* Call this during shutdown, **before** `TTF_Quit` or `SDL_Quit`. Afterwards
* #AKGL_REGISTRY_FONT is 0 and akgl_text_loadfont needs
* akgl_registry_init_font() again before it can register anything.
*
* @return `NULL`. Closing a font reports nothing, and an uninitialized registry
* is success rather than an error -- shutdown paths run after partial
* startups.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_text_unloadallfonts(void);
/**
* @brief Rasterize a string and blit it at a screen position, in one call.
*