Files
libakgl/include/akgl/util.h
Andrew Kesterson 19530f6a97 Fix the type, macro and state-table defects, and the leftover debris
Closes internal-consistency items 19 through 36, 38 and 41. Item 37, the ~180
redundant casts, is deliberately left open with its reasoning in TODO.md: the
benefit only arrives once the build turns on the warnings those casts suppress,
and doing it before that is churn across the two files with the most
outstanding functional defects.

The two that were real bugs are in the actor state table.
AKGL_ACTOR_STATE_STRING_NAMES was declared [AKGL_ACTOR_MAX_STATES+1] and
defined [32], so a consumer trusting the declared bound read past the object;
and indices 11 and 12 were named UNDEFINED_11 and UNDEFINED_12 where actor.h
has MOVING_IN and MOVING_OUT, so no character JSON could bind a sprite to
either state. tests/registry.c now walks the whole table -- every entry
non-NULL, every entry resolving to its own bit, no two entries sharing a name.

The bitmask macros are parenthesized and AKGL_BITMASK_CLEAR has lost the
semicolon inside its body. Writing tests/bitmasks.c for that turned up
something worth knowing: the obvious test does not catch it. For a bit that is
set, the misparse `!(mask & bit) == bit` gives the same answer as the correct
one. It only diverges for an unset bit whose value is not 1, and that is the
shape the suite uses now.

akgl_draw_background was the last public function outside the error protocol.
It takes a backend like everything else in draw.h, restores the draw colour it
found, and is tested -- TODO.md had it filed under "needs the offscreen
renderer harness", which was never true; what it needed was to stop reading the
global.

All eight registry initializers go through one helper, so the seven that leaked
an SDL_PropertiesID on every call after the first no longer do. Fixed in the
same place because it is the same function: akgl_registry_init never called
akgl_registry_init_properties, which made akgl_set_property a silent no-op for
anyone not going through akgl_game_init -- Defects, Known and still open item 3.

Also: AKGL_COLLIDE_RECTANGLES (three open parens, two closes) and akgl_Frame
deleted, float32_t/float64_t used consistently, the developer-specific debug
logging removed from the controller inner loop, the abandoned SDL_GetBasePath
comments removed, nine unused locals removed, and dst renamed to dest.

akgl_game_update's default flags no longer OR the same bit twice. That changes
nothing today, and the reason is Performance item 32: the loop never reads
either bit, which is why every actor is updated sixteen times a frame. Still
open.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:33:36 -04:00

187 lines
9.1 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`.
* @throws AKERR_VALUE If the 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.
*/
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.
*
* @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.
*/
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_