It asked whether either rectangle enclosed one of the other's four corners -- eight akgl_collide_point_rectangle calls, stopping at the first hit. That is a different question from "do these overlap", and it has the wrong answer for one arrangement: a tall thin rectangle crossing a short wide one overlaps in a plus sign with no corner of either inside the other, and all eight tests said no. A long thin platform crossing a tall thin character is exactly that shape, so this is a shape a 2D game produces, not a curiosity. util.h carried an @note describing it and docs/18-utilities.md had a diagram of it, both under the heading of a limitation rather than a defect, and there was no test for it at all -- nor for full containment, nor for a shared edge. It is four comparisons now, on both axes. `<=` rather than `<` because akgl_collide_point_rectangle is inclusive on all four edges and these two have always agreed that touching counts; a span test written with `<` would have silently changed a contract both the header and the manual state. The test was written first and failed on the cross before the fix went in. Two answers change for a caller upgrading, and both are in the header note and the chapter: - The cross reports `true`, which is the point. - The comparison is in float rather than through akgl_Point's int members, so a sub-pixel overlap is no longer truncated away. A pickup test that was accidentally forgiving by up to a pixel is no longer forgiving. Both tutorials use this for coins and hazards; both still pass. Faster as a side effect rather than a goal, and worth recording because the numbers move a documented budget: 24.9 ns -> 6.1 overlapping, 57.9 -> 6.1 disjoint, and the all-pairs sweep over 64 actors 115 us -> 12.2. The disjoint case gained most because it was the one that ran all eight tests before answering. The three moved rows are re-recorded in PERFORMANCE.md and nothing else is. akgl_rectangle_points is untouched by this change and reads 6.1 in the same run against the 4.0 recorded, so 6 ns is this run's floor and the new figure means "too cheap to measure" rather than "exactly 6.1" -- said in the prose so the next reader does not re-baseline the table around it. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
285 lines
11 KiB
C
285 lines
11 KiB
C
/**
|
|
* @file util.c
|
|
* @brief Implements the util subsystem.
|
|
*/
|
|
|
|
#include <limits.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <libgen.h>
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <akerror.h>
|
|
|
|
#include <akgl/util.h>
|
|
#include <akgl/heap.h>
|
|
#include <akgl/registry.h>
|
|
#include <akgl/game.h>
|
|
#include <akgl/staticstring.h>
|
|
|
|
#include <akstdlib.h>
|
|
|
|
/**
|
|
* @brief Resolve @p path against @p root and write the absolute result to @p dest.
|
|
*
|
|
* The second half of akgl_path_relative: joins the two with a `/` and resolves
|
|
* the join, so symlinks and `..` are folded out and the result is absolute. It
|
|
* is the fallback, reached only once resolving @p path on its own has failed.
|
|
*
|
|
* `static`: it is reachable only through akgl_path_relative, which is the only
|
|
* caller and the only sensible one.
|
|
*
|
|
* @param root Directory to resolve against, normally `dirname` of the file that
|
|
* named @p path. Required. A trailing `/` is harmless; the join adds
|
|
* one unconditionally and `realpath` collapses the double.
|
|
* @param path Relative path to resolve. Required. An absolute @p path still gets
|
|
* @p root pasted in front of it, which will not exist -- so callers
|
|
* must not send absolute paths down this branch.
|
|
* @param dest Receives the resolved absolute path. Required, and must already be
|
|
* a claimed pool string.
|
|
* @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` is at least #AKGL_MAX_STRING_LENGTH
|
|
* bytes. The message reports both the combined length and the limit.
|
|
* @throws ENOENT If the joined path does not exist. Any other `errno`
|
|
* `realpath(3)` raises -- EACCES, ELOOP, ENOTDIR -- propagates likewise.
|
|
* @throws AKGL_ERR_HEAP If the string pool cannot supply the two scratch buffers.
|
|
*
|
|
* @note The AKERR_OUTOFBOUNDS check uses `FAIL_RETURN` from inside the `ATTEMPT`
|
|
* block, which returns past `CLEANUP` -- so on that one path the two
|
|
* scratch strings are never released. This is exactly the hazard AGENTS.md
|
|
* warns about; it wants a `FAIL_BREAK`.
|
|
*/
|
|
static akerr_ErrorContext *path_relative_root(char *root, char *path, akgl_String *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_String *pathbuf;
|
|
akgl_String *strbuf;
|
|
int rootlen;
|
|
int pathlen;
|
|
int count;
|
|
|
|
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
|
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");
|
|
|
|
PASS(errctx, akgl_heap_next_string(&strbuf));
|
|
PASS(errctx, akgl_heap_next_string(&pathbuf));
|
|
|
|
ATTEMPT {
|
|
// Is it relative to the root?
|
|
rootlen = strlen(root);
|
|
pathlen = strlen(path);
|
|
if ( (rootlen + pathlen) >= AKGL_MAX_STRING_LENGTH ) {
|
|
FAIL_BREAK(errctx, AKERR_OUTOFBOUNDS, "Total path length (%d) is greater than maximum akgl_String length (%d)", (rootlen + pathlen), AKGL_MAX_STRING_LENGTH);
|
|
}
|
|
CATCH(errctx, aksl_snprintf(
|
|
&count,
|
|
(char *)&pathbuf->data,
|
|
sizeof(pathbuf->data),
|
|
"%s/%s",
|
|
root,
|
|
path
|
|
));
|
|
CATCH(errctx, aksl_realpath((char *)&pathbuf->data, (char *)&strbuf->data, sizeof(strbuf->data)));
|
|
CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(strbuf));
|
|
IGNORE(akgl_heap_release_string(pathbuf));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akgl_String *strbuf;
|
|
bool relative_to_root = false;
|
|
|
|
FAIL_ZERO_RETURN(errctx, root, AKERR_NULLPOINTER, "NULL argument");
|
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "NULL argument");
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL argument");
|
|
|
|
PASS(errctx, akgl_heap_next_string(&strbuf));
|
|
|
|
ATTEMPT {
|
|
// Is path relative to our current working directory?
|
|
CATCH(errctx, aksl_realpath(path, (char *)&strbuf->data, sizeof(strbuf->data)));
|
|
// Yes it is. strbuf->data contains the absolute path.
|
|
CATCH(errctx, akgl_string_copy(strbuf, dest, 0));
|
|
} CLEANUP {
|
|
IGNORE(akgl_heap_release_string(strbuf));
|
|
} PROCESS(errctx) {
|
|
} HANDLE(errctx, ENOENT) {
|
|
// Path is not relative to our current working directory. Resolve it
|
|
// against root instead -- but after FINISH, not from in here. Returning
|
|
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
|
|
// with, so the handled context is never given back to
|
|
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
|
|
// call takes the whole process down with "Unable to pull an error
|
|
// context from the array!". Every map load resolves several paths this
|
|
// way.
|
|
relative_to_root = true;
|
|
} FINISH(errctx, true);
|
|
|
|
if ( relative_to_root == true ) {
|
|
PASS(errctx, path_relative_root(root, path, dest));
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_rectangle_points(akgl_RectanglePoints *dest, SDL_FRect *rect)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference");
|
|
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL Rectangle reference");
|
|
dest->topleft.x = rect->x;
|
|
dest->topleft.y = rect->y;
|
|
dest->bottomleft.x = rect->x;
|
|
dest->bottomleft.y = rect->y + rect->h;
|
|
dest->topright.x = rect->x + rect->w;
|
|
dest->topright.y = rect->y;
|
|
dest->bottomright.x = rect->x + rect->w;
|
|
dest->bottomright.y = rect->y + rect->h;
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_collide_point_rectangle(akgl_Point *p, akgl_RectanglePoints *rp, bool *collide)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, p, AKERR_NULLPOINTER, "NULL Point reference");
|
|
FAIL_ZERO_RETURN(errctx, rp, AKERR_NULLPOINTER, "NULL akgl_RectanglePoints reference");
|
|
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL boolean reference");
|
|
if ( (p->x >= rp->topleft.x) && (p->y >= rp->topleft.y) &&
|
|
(p->x <= rp->bottomright.x) && (p->y <= rp->bottomright.y) ) {
|
|
*collide = true;
|
|
} else {
|
|
*collide = false;
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, r1, AKERR_NULLPOINTER, "NULL rectangle reference");
|
|
FAIL_ZERO_RETURN(errctx, r2, AKERR_NULLPOINTER, "NULL rectangle reference");
|
|
FAIL_ZERO_RETURN(errctx, collide, AKERR_NULLPOINTER, "NULL collision flag reference");
|
|
|
|
/*
|
|
* Two rectangles overlap when their spans overlap on both axes. This used to
|
|
* ask a different question -- whether either rectangle enclosed a corner of
|
|
* the other, eight akgl_collide_point_rectangle calls -- and that question
|
|
* has a different answer for a tall thin rectangle crossing a short wide
|
|
* one. They overlap in a plus sign, neither holds a corner of the other, and
|
|
* all eight tests said no. util.h carried a @note describing the arrangement
|
|
* rather than a fix.
|
|
*
|
|
* `<=` and not `<`, because akgl_collide_point_rectangle is inclusive on all
|
|
* four edges and this function has always agreed with it: two rectangles
|
|
* sharing exactly one edge collide.
|
|
*
|
|
* The comparison is in float now. The corner form went through akgl_Point,
|
|
* whose members are int, so a rectangle at x = 10.9 had its corner at 10 and
|
|
* a sub-pixel overlap was invisible.
|
|
*/
|
|
*collide = ( (r1->x <= (r2->x + r2->w)) &&
|
|
(r2->x <= (r1->x + r1->w)) &&
|
|
(r1->y <= (r2->y + r2->h)) &&
|
|
(r2->y <= (r1->y + r1->h)) );
|
|
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
|
|
akerr_ErrorContext *akgl_compare_sdl_surfaces(SDL_Surface *s1, SDL_Surface *s2)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
FAIL_ZERO_RETURN(errctx, s1, AKERR_NULLPOINTER, "NULL Surface pointer");
|
|
FAIL_ZERO_RETURN(errctx, s2, AKERR_NULLPOINTER, "NULL Surface pointer");
|
|
// Geometry first. The memcmp reads s1->pitch * s1->h bytes out of *both*,
|
|
// so a smaller s2 was read past its end rather than reported as a
|
|
// mismatch -- and a differing pitch or format made the comparison
|
|
// meaningless even when it did not run off.
|
|
FAIL_NONZERO_RETURN(
|
|
errctx,
|
|
((s1->w != s2->w) || (s1->h != s2->h)),
|
|
AKERR_VALUE,
|
|
"Comparison surfaces differ in size: %dx%d against %dx%d",
|
|
s1->w, s1->h, s2->w, s2->h);
|
|
FAIL_NONZERO_RETURN(
|
|
errctx,
|
|
(s1->pitch != s2->pitch),
|
|
AKERR_VALUE,
|
|
"Comparison surfaces differ in pitch: %d against %d",
|
|
s1->pitch, s2->pitch);
|
|
FAIL_NONZERO_RETURN(
|
|
errctx,
|
|
(s1->format != s2->format),
|
|
AKERR_VALUE,
|
|
"Comparison surfaces differ in pixel format: %s against %s",
|
|
SDL_GetPixelFormatName(s1->format), SDL_GetPixelFormatName(s2->format));
|
|
FAIL_ZERO_RETURN(errctx, s1->pixels, AKERR_NULLPOINTER, "First surface has no pixels");
|
|
FAIL_ZERO_RETURN(errctx, s2->pixels, AKERR_NULLPOINTER, "Second surface has no pixels");
|
|
FAIL_NONZERO_RETURN(errctx, memcmp(s1->pixels, s2->pixels, (s1->pitch * s1->h)), AKERR_VALUE, "Comparison surfaces are not equal");
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
akerr_ErrorContext *akgl_render_and_compare(SDL_Texture *t1, SDL_Texture *t2, int x, int y, int w, int h, char *writeout)
|
|
{
|
|
SDL_Surface *s1 = NULL;
|
|
SDL_Surface *s2 = NULL;
|
|
SDL_FRect src = {.x = x, .y = y, .w = w, .h = h};
|
|
SDL_FRect dest = {.x = x, .y = y, .w = w, .h = h};
|
|
SDL_Rect read = {.x = x, .y = y, .w = w, .h = h};
|
|
akgl_String *tmpstring = NULL;
|
|
int count = 0;
|
|
|
|
PREPARE_ERROR(errctx);
|
|
ATTEMPT {
|
|
FAIL_ZERO_BREAK(errctx, t1, AKERR_NULLPOINTER, "NULL texture");
|
|
FAIL_ZERO_BREAK(errctx, t2, AKERR_NULLPOINTER, "NULL texture");
|
|
|
|
CATCH(errctx, akgl_heap_next_string(&tmpstring));
|
|
SDL_RenderClear(akgl_renderer->sdl_renderer);
|
|
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t1, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
|
s1 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
|
|
FAIL_ZERO_BREAK(errctx, s1, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
|
|
|
if ( writeout != NULL ) {
|
|
CATCH(errctx, aksl_snprintf(
|
|
&count,
|
|
(char *)&tmpstring->data,
|
|
sizeof(tmpstring->data),
|
|
"%s%s",
|
|
SDL_GetBasePath(),
|
|
writeout));
|
|
FAIL_ZERO_BREAK(
|
|
errctx,
|
|
IMG_SavePNG(s1, (char *)&tmpstring->data),
|
|
AKERR_IO,
|
|
"Unable to save %s: %s",
|
|
(char *)&tmpstring->data,
|
|
SDL_GetError());
|
|
}
|
|
|
|
SDL_RenderClear(akgl_renderer->sdl_renderer);
|
|
|
|
CATCH(errctx, akgl_renderer->draw_texture(akgl_renderer, t2, &src, &dest, 0, NULL, SDL_FLIP_NONE));
|
|
s2 = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, &read);
|
|
FAIL_ZERO_BREAK(errctx, s2, AKGL_ERR_SDL, "Failed to read pixels from renderer");
|
|
|
|
CATCH(errctx, akgl_compare_sdl_surfaces(s1, s2));
|
|
} CLEANUP {
|
|
if ( s1 != NULL )
|
|
SDL_DestroySurface(s1);
|
|
if ( s2 != NULL )
|
|
SDL_DestroySurface(s2);
|
|
IGNORE(akgl_heap_release_string(tmpstring));
|
|
} PROCESS(errctx) {
|
|
} FINISH(errctx, true);
|
|
SUCCEED_RETURN(errctx);
|
|
}
|