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:
68
TODO.md
68
TODO.md
@@ -904,39 +904,33 @@ without coming here first. Ordered by blast radius.
|
||||
does not cover a binding replaced while the character is alive.
|
||||
|
||||
22. **`akgl_path_relative_root` uses `FAIL_RETURN` inside its `ATTEMPT` block.**
|
||||
`src/util.c:75` returns past `CLEANUP` on the over-long-path branch, so the
|
||||
two scratch strings claimed at `src/util.c:67-68` are never released. This is
|
||||
the exact hazard AGENTS.md documents under the error-handling protocol, and
|
||||
the same class as the heap-string leak already fixed in
|
||||
`akgl_get_json_tilemap_property`. Smaller blast radius than item 15 because
|
||||
the branch is rare, but it is a mechanical fix.
|
||||
**Fixed in 0.5.0** with internal-consistency item 16, which swept every
|
||||
such site. It is `FAIL_BREAK`, and `path_relative_root` is `static` now.
|
||||
`scripts/check_error_protocol.py` fails the build if one comes back.
|
||||
|
||||
Fix: `FAIL_BREAK`. Touches `src/util.c:75`.
|
||||
23. **Three smaller leaks on failure paths.** **All three fixed in 0.5.0**, each
|
||||
by moving the release into a `CLEANUP` block.
|
||||
|
||||
23. **Three smaller leaks on failure paths.** All the same shape: a resource
|
||||
acquired before an `ATTEMPT`/`CLEANUP` pair, or released only on the success
|
||||
path.
|
||||
|
||||
- `src/renderer.c:26-32` — `akgl_render_init2d` releases the two pooled
|
||||
strings holding the screen dimensions only after both `aksl_atoi` calls
|
||||
succeed, so a non-numeric `game.screenwidth` leaks two string slots.
|
||||
- `src/controller.c:80` — `akgl_controller_open_gamepads` calls
|
||||
`SDL_free(gamepads)` only after the loop completes, so a gamepad that
|
||||
fails to open leaks the enumeration array.
|
||||
- `src/text.c:57-64` — `akgl_text_rendertextat` destroys the surface and
|
||||
texture only on the success path, so a failed texture upload or a failed
|
||||
draw leaks both. On a HUD line redrawn every frame that is a leak per
|
||||
frame.
|
||||
- `akgl_render_2d_init` released its two pooled strings only after both
|
||||
`aksl_atoi` calls succeeded, so a non-numeric `game.screenwidth` leaked
|
||||
two of the pool's 256 entries. `tests/renderer.c` runs 512 failing
|
||||
initializations against each of the two properties and asserts the pool is
|
||||
unchanged; against the old code the first loop claims every slot.
|
||||
- `akgl_controller_open_gamepads` freed the enumeration array only after the
|
||||
loop completed, so a gamepad that failed to open took the array with it.
|
||||
The open failure is recorded and reported after the loop, because a
|
||||
`FAIL_ZERO_BREAK` inside it would have broken the loop rather than the
|
||||
block. It also frees the array on the "no gamepads enumerated" path, which
|
||||
SDL still allocates for.
|
||||
- `akgl_text_rendertextat` destroyed the surface and texture only on the
|
||||
success path, so a failed upload leaked the surface and a failed draw
|
||||
leaked both -- once per frame on a HUD line.
|
||||
|
||||
24. **`akgl_get_property` reads past the end of the property value.**
|
||||
`src/registry.c:182` copies a fixed `AKGL_MAX_STRING_LENGTH` bytes out of
|
||||
whatever `SDL_GetStringProperty` returned, rather than that string's length.
|
||||
The destination is a full-sized pool string so nothing is corrupted, but the
|
||||
source is an ordinary NUL-terminated string owned by SDL and the read runs
|
||||
up to PATH_MAX bytes past its end. Benign in practice and immediately fatal
|
||||
under ASAN, which is the reason to fix it rather than leave it.
|
||||
|
||||
Fix: `aksl_strlcpy` or an explicit length. Touches `src/registry.c:181-186`.
|
||||
**Already fixed**, as **Defects the memory checker found** item 35 -- the
|
||||
two entries are the same defect found twice, from reading the code and from
|
||||
running valgrind. Recorded here only so the duplicate does not read as
|
||||
outstanding.
|
||||
|
||||
25. **`akgl_character_load_json_state_int_from_strings` checks the same argument
|
||||
twice.** `src/character.c:123` guards `states` and `src/character.c:124`
|
||||
@@ -1807,14 +1801,14 @@ this library and told it is compatible.
|
||||
each reference, destroys `state_sprites`, and then clears the character, and
|
||||
`akgl_character_state_sprites_iterate` is covered by that path.
|
||||
|
||||
**Still open: removal and replacement.** There is no API to unbind one state,
|
||||
and `akgl_character_sprite_add` still overwrites an existing entry without
|
||||
releasing the sprite it displaces (**Defects** item 20). A character that
|
||||
rebinds a state while alive still leaks one reference per rebind; teardown
|
||||
only gives back whatever the map happens to hold at the end. Wants
|
||||
`akgl_character_sprite_del`, a release of the displaced entry inside
|
||||
`akgl_character_sprite_add`, and tests for removal, replacement, and
|
||||
duplicate sprite bindings across multiple states.
|
||||
**Replacement is done too**, as **Defects** item 20:
|
||||
`akgl_character_sprite_add` releases the sprite it displaces, and
|
||||
`tests/character.c` covers binding, rebinding, and 200 alternating rebinds.
|
||||
|
||||
**Still open: removal.** There is no API to unbind one state without binding
|
||||
something else over it -- `akgl_character_sprite_del` -- and no test for
|
||||
duplicate sprite bindings across several states. Neither leaks anything
|
||||
today; they are a gap in the surface rather than a defect.
|
||||
|
||||
2. **An actor cannot be scaled per axis.** `akgl_Actor::scale` is one `float32_t` applied
|
||||
to both `dest.w` and `dest.h`, so there is no way to express "twice as wide, the same
|
||||
|
||||
Reference in New Issue
Block a user