Report the failures that used to be crashes

Closes Defects items 30 and 31 and Known-and-still-open items 1, 2, 5, 9 and 11.

Both string accessors in json_helpers.c ended their ATTEMPT block with
FINISH(errctx, false), which swallows the failure, and then strncpy'd through
the pointer akgl_heap_next_string never set. So the one condition the pool
exists to report -- it is full, which in practice means something is not
releasing -- arrived as a segfault somewhere else entirely. It is
FINISH(errctx, true) now, and tests/json_helpers.c claims every slot and
asserts AKGL_ERR_HEAP comes back out of both. That test segfaults against the
old code, which is also how the tilemap leak test in the previous commit
confirmed this one.

akgl_tilemap_release tested layers[i].texture and destroyed
tilesets[i].texture, so every tileset texture was freed twice on one release
and no image layer's texture was freed at all. Pointers are cleared as they go,
so a second release is safe instead of a use-after-free.

akgl_game_update_fps called game.lowfpsfunc() unguarded, on a path taken on
frame one because fps is 0 for the first second. Only akgl_game_init installs
it, and renderer.h documents the other path deliberately -- a host that owns
its window binds a backend instead. It installs the default when it finds NULL.

akgl_controller_pushmap and akgl_controller_default checked only the upper
bound, so a negative id indexed before akgl_controlmaps.

The two test-harness helpers were quietly worthless. akgl_render_and_compare
drew t1 on both passes, so it always reported a match and every image assertion
built on it asserted nothing; and akgl_compare_sdl_surfaces memcmp'd
s1->pitch * s1->h bytes out of both surfaces without checking that the second
was the same size, so a smaller one was read past its end. Both fixed, both
tested. tests/util.c also now calls the collide-point test it has defined and
never run.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:32:11 -04:00
parent 230278d303
commit 21d69192e3
12 changed files with 309 additions and 15 deletions

View File

@@ -92,7 +92,7 @@ typedef struct {
SDL_Time lastIterTime; /**< Timestamp of the most recent akgl_game_update_fps call. */
SDL_Time lastFPSTime; /**< When `fps` was last recomputed. */
int16_t framesSinceUpdate; /**< Frames counted so far in the current second. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. Defaults to akgl_game_lowfps; replace it to do something more useful than log. */
void (*lowfpsfunc)(void); /**< Called every frame while `fps` is under 30. akgl_game_init installs akgl_game_lowfps, and akgl_game_update_fps installs it too if it finds this NULL -- an embedder that binds its own renderer rather than calling akgl_game_init used to crash here on frame one. Replace it to do something more useful than log. */
} akgl_Game;
/** @brief The SDL window, created by akgl_render_2d_init. `NULL` until then. */

View File

@@ -142,12 +142,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, ak
* @param s2 Second surface. Required.
* @return `NULL` when the pixels match, otherwise an error context owned by the
* caller. "Not equal" is reported as an error, not as an out-param.
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`.
* @throws AKERR_VALUE If the pixels differ.
* @throws AKERR_NULLPOINTER If @p s1 or @p s2 is `NULL`, or either has no
* pixel buffer.
* @throws AKERR_VALUE If the surfaces differ in size, pitch, or pixel format,
* or if their pixels differ.
*
* @warning The surfaces' dimensions, pitch, and format are not compared, so a
* smaller @p s2 is read past its end rather than reported as a
* mismatch. TODO.md, "Known and still open" item 5.
* Dimensions, pitch and pixel format are compared first, and a difference in
* any of them is reported as a mismatch. Until 0.5.0 they were not, so a
* smaller @p s2 was read past its end instead -- benign in practice and
* immediately fatal under a memory checker.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2);
/**
@@ -177,9 +180,9 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_compare_sdl_surfaces(SDL_Surface *s1, SD
* @throws AKERR_VALUE If the two renders differ.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @warning Known defect: both passes draw @p t1 -- @p t2 is never rendered -- so
* this currently always reports a match. TODO.md, "Known and still
* open" item 1.
* @note Until 0.5.0 both passes drew @p t1, so this always reported a match and
* every image assertion built on it asserted nothing. It draws @p t2 on
* the second pass now.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout);