Files
libakgl/include/akgl/util.h
Andrew Kesterson 21d69192e3 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>
2026-08-01 07:33:37 -04:00

190 lines
9.3 KiB
C

/**
* @file util.h
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
*
* The grab bag. Three unrelated groups live here: rectangle/point overlap for
* the physics backend, path resolution for the asset loaders, and a pair of
* pixel-comparison routines that exist only so tests can assert on what was
* actually drawn.
*
* All the geometry here is axis-aligned and treats edges as touching: a point
* exactly on a boundary is inside. There is no rotation support and no
* separating-axis test.
*/
#ifndef _AKGL_UTIL_H_
#define _AKGL_UTIL_H_
#include <SDL3/SDL.h>
#include <akerror.h>
#include <stdbool.h>
#include <akgl/staticstring.h>
/** @brief An integer point. Carries a `z` the collision routines do not use. */
typedef struct akgl_Point {
int x; /**< Horizontal position, in whatever space the caller is working in. */
int y; /**< Vertical position. */
int z; /**< Depth. Never written by akgl_rectangle_points and never read by the collision tests. */
} akgl_Point;
/**
* @brief The four corners of an axis-aligned rectangle, precomputed.
*
* akgl_collide_rectangles works corner by corner rather than by comparing edge
* spans, so it wants the corners as points. akgl_rectangle_points derives one of
* these from an `SDL_FRect`.
*/
typedef struct akgl_RectanglePoints {
akgl_Point topleft; /**< (x, y). */
akgl_Point topright; /**< (x + w, y). */
akgl_Point bottomleft; /**< (x, y + h). */
akgl_Point bottomright; /**< (x + w, y + h). */
} akgl_RectanglePoints;
/**
* @brief Expand a rectangle into its four corner points.
*
* Coordinates are truncated from `float` to `int` on the way in, so a rectangle
* at x = 10.9 has its corners at 10. That is deliberate for tile-grid work and
* wrong for sub-pixel work; callers needing the latter should not round-trip
* through this.
*
* @param dest Receives the corners. Required.
* @param rect The rectangle, in any coordinate space. Required. `w` and `h` are
* taken as extents from `x`/`y`, so a negative one produces a
* rectangle whose "bottom right" is above and left of its "top
* left" -- which every test here then reports as empty.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p dest or @p rect is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect);
/**
* @brief Test whether a point falls inside a rectangle, edges included.
*
* Compares against `topleft` and `bottomright` only, so it assumes @p r is
* well-formed -- the two corners actually being the minimum and maximum. `z` is
* ignored on both sides: this is a 2D test.
*
* @param p The point to test. Required.
* @param r The rectangle, as corners from akgl_rectangle_points. Required.
* @param collide Receives `true` when the point is inside or exactly on an edge,
* `false` otherwise. Required -- the return value is the error
* context. Not written on any failure path.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p p, @p r, or @p collide is `NULL`.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *r, bool *collide);
/**
* @brief Test whether two rectangles overlap, edges included.
*
* Tests all eight corners -- each rectangle's four against the other -- and
* stops at the first hit. Checking both directions is what catches the case
* where one rectangle is entirely inside the other and so has no corner within
* its neighbour.
*
* @param r1 First rectangle. Required.
* @param r2 Second rectangle. Required. Order does not matter.
* @param collide Receives `true` on any overlap or shared edge, `false`
* otherwise. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p r1, @p r2, or @p collide is `NULL`.
*
* @note A corner-containment test misses the one arrangement where two
* rectangles overlap in a cross without either enclosing a corner of the
* other -- a tall thin rectangle crossing a short wide one. Both are
* reported as not colliding.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
/**
* @brief Resolve an asset path, trying the working directory before the given root.
*
* Asset files name their neighbours relatively -- a sprite definition names its
* spritesheet, a tilemap names its tilesets -- and "relative" has to mean
* relative to the file doing the naming, not to wherever the game was launched
* from. So this tries @p path against the process working directory first, and
* only if that does not exist joins it onto @p root and resolves that. Either
* way the result is absolute, with symlinks and `..` folded out.
*
* @param root Directory to fall back to, normally `dirname` of the file that
* contained @p path. Required, even when unused.
* @param path The path to resolve, relative or absolute. Required.
* @param dest Receives the resolved absolute path. Required, and must already be
* a claimed pool string -- this writes into it, it does not claim
* one for you.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p root, @p path, or @p dest is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `root + "/" + path` would not fit in
* #AKGL_MAX_STRING_LENGTH.
* @throws ENOENT If neither spelling names an existing file. Any other `errno`
* `realpath(3)` can raise -- EACCES on an unsearchable directory,
* ELOOP, ENOTDIR -- propagates the same way.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @note The fallback path -- the common one, since most asset references are
* relative to their own file rather than to the working directory --
* returns straight out of the ENOENT handler and so never releases the
* error context it was handling. Each such call consumes one slot of
* libakerror's fixed 128-entry context array for the life of the process.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_path_relative(char *root, char *path, akgl_String *dest);
// These are REALLY slow routines that are only useful in testing harnesses
/**
* @brief Assert that two surfaces hold byte-identical pixels.
*
* A `memcmp` over the raw pixel buffer, so it is exact: one differing byte in
* one pixel is a failure. Meant for test harnesses asserting on rendered output,
* not for anything on a frame path.
*
* @param s1 First surface. Required. Its `pitch * h` is what determines how many
* bytes are compared.
* @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`, or either has no
* pixel buffer.
* @throws AKERR_VALUE If the surfaces differ in size, pitch, or pixel format,
* or if their pixels differ.
*
* 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);
/**
* @brief Draw two textures in turn, read the framebuffer back after each, and compare.
*
* The test-harness counterpart to akgl_compare_sdl_surfaces: it answers "do
* these two textures *render* the same", which is not the same question as "are
* these two textures identical", because the renderer's scaling and blending sit
* in between. Both are drawn into the same rectangle against a cleared target.
*
* @param t1 First texture. Required.
* @param t2 Second texture. Required.
* @param x Left edge of the region, in pixels. Used for the source
* rectangle, the destination, and the readback alike.
* @param y Top edge of the region.
* @param w Width of the region.
* @param h Height of the region.
* @param writeout Optional filename for a PNG of the *first* render, written
* under `SDL_GetBasePath()`. `NULL` skips it. This is a
* debugging aid -- when an image assertion fails, this is how
* you see what was actually drawn.
* @return `NULL` when the two renders match, otherwise an error context owned by
* the caller.
* @throws AKERR_NULLPOINTER If @p t1 or @p t2 is `NULL`.
* @throws AKGL_ERR_SDL If the framebuffer cannot be read back.
* @throws AKERR_IO If @p writeout is given and the PNG cannot be written.
* @throws AKERR_VALUE If the two renders differ.
* @throws AKGL_ERR_HEAP If the string pool is exhausted.
*
* @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);
#endif // _AKGL_UTIL_H_