Remove the corner helpers akgl_collide_rectangles no longer uses
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 21s
libakgl CI Build / performance (push) Failing after 21s
libakgl CI Build / memory_check (push) Failing after 17s
libakgl CI Build / mutation_test (push) Failing after 19s

akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and
akgl_RectanglePoints go. They were the intermediate form of an implementation
that changed: akgl_collide_rectangles was eight corner-containment tests built on
them, and it has been four span comparisons since the cross-case fix. Nothing
outside tests/ called either function, and a point-in-rectangle test is four
comparisons a caller can write without a struct conversion in front of them.

akgl_collide_rectangles stays. It has two correct callers in the sidescroller
asking a game-level overlap question -- a coin, a hazard, from an updatefunc --
where a bool is the whole answer and a proxy plus a narrowphase call would be
computing a normal nothing reads. TODO.md records the split rather than leaving
it to be rediscovered.

Public API removal, so 194 exported akgl_ symbols against 196, and the manual's
counts move with them. The perf suite loses its rectangle_points row; the
all-pairs sweep stays as the control it is now labelled, and PERFORMANCE.md says
what 0.8.0 measured against it -- 188.5 us for 32,640 pairs at 256 actors, where
a whole step with collision attached is 54.1 us doing strictly more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
This commit is contained in:
2026-08-02 08:09:42 -04:00
parent 3a6569e384
commit 4e32328681
11 changed files with 134 additions and 395 deletions

View File

@@ -758,14 +758,6 @@ static akerr_ErrorContext *bench_logic_frame(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief Time the geometry helpers, including the all-pairs sweep a caller has to write.
*
* akgl_collide_rectangles tests eight corners and returns at the first hit, so
* an overlap is cheap and a miss is the full eight. The third benchmark is the
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
* caller doing collision at all does that every frame.
*/
/** @brief Counts nothing; the query cost is what is being measured. */
static akerr_ErrorContext *bench_collision_visit(akgl_CollisionProxy *proxy, void *data)
{
@@ -895,11 +887,22 @@ static akerr_ErrorContext *bench_collision(void)
SUCCEED_RETURN(errctx);
}
/**
* @brief Time akgl_collide_rectangles, and the all-pairs sweep it used to imply.
*
* The function is four span comparisons in `float32_t`, so an overlap and a miss
* cost the same -- it has no early exit and does not want one.
*
* The last benchmark is the **control**, not a recommendation. It is what a game
* had to write before 0.8.0: 64 actors is 2016 pairs, every frame. The
* partitioner rows in bench_collision are what to compare it against, and the
* comparison is generous to the control -- it tests pairs and does not sync,
* sub-step or resolve anything.
*/
static akerr_ErrorContext *bench_geometry(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
akgl_RectanglePoints points;
SDL_FRect rects[BENCH_ACTOR_COUNT];
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
@@ -922,11 +925,6 @@ static akerr_ErrorContext *bench_geometry(void)
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
bench_start("rectangle_points", "call", 100.0);
BENCH_LOOP(inner, i, count, akgl_rectangle_points(&points, &subject));
bench_stop(count);
PASS(errctx, inner);
bench_start("collide_rectangles, overlapping", "call", 300.0);
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
bench_stop(count);

View File

@@ -28,172 +28,6 @@ static int live_error_contexts(void)
return live;
}
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
{
akgl_RectanglePoints points;
// Zeroed for the same reason as the fixtures in
// test_akgl_collide_point_rectangle_nullpointers: the last case here is a
// real call, and feeding it stack garbage is noise under `memcheck`.
SDL_FRect testrect = {.x = 0, .y = 0, .w = 0, .h = 0};
PREPARE_ERROR(errctx);
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with all NULL pointers");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(NULL, &testrect));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL SDL_FRect pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_rectangle_points fails to FAIL with NULL akgl_RectanglePoints pointer");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_rectangle_points_math(void)
{
akgl_RectanglePoints points;
SDL_FRect testrect = {.x = 0, .y = 0, .w = 32, .h = 32};
memset((void *)&points, 0x00, sizeof(akgl_RectanglePoints));
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&points, &testrect));
if ( points.topleft.x != 0 ||
points.topleft.y != 0 ||
points.topright.x != 32 ||
points.topright.y != 0 ||
points.bottomleft.x != 0 ||
points.bottomleft.y != 32 ||
points.bottomright.x != 32 ||
points.bottomright.y != 32 ) {
FAIL_BREAK(
errctx,
AKGL_ERR_BEHAVIOR,
"akgl_rectangle_points incorrectly calculated points for {x=0, y=0, w=32, h=32} to {topleft={%d, %d}, topright={%d, %d}, bottomleft={%d, %d}, bottomright={%d, %d}}",
points.topleft.x, points.topleft.y,
points.topright.x, points.topright.y,
points.bottomleft.x, points.bottomleft.y,
points.bottomright.x, points.bottomright.y
);
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_point_rectangle_nullpointers(void)
{
// Zeroed rather than left as whatever the stack held. The last case in this
// function is a real call with real arguments, and reading uninitialised
// floats out of it is sixteen findings under `memcheck` for a test that is
// not about coordinates at all.
akgl_Point testpoint = { .x = 0, .y = 0 };
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
memset(&testrectpoints, 0x00, sizeof(akgl_RectanglePoints));
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, *, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, NULL, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(*, NULL, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, &testrectpoints, &testcollide));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, *, *) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(NULL, NULL, NULL));
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "akgl_collide_point_rectangle(NULL, NULL, NULL) failed");
} CLEANUP {
} PROCESS(errctx) {
} HANDLE(errctx, AKERR_NULLPOINTER) {
// noop
} FINISH(errctx, true);
ATTEMPT {
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_point_rectangle_logic(void)
{
akgl_Point testpoint = {.x = 16, .y = 16};
SDL_FRect testrect = { .x = 0, .y = 0, .w = 32, .h = 32};
akgl_RectanglePoints testrectpoints;
bool testcollide = false;
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_rectangle_points(&testrectpoints, &testrect));
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == false ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Valid collision missed");
}
testpoint.x = 48;
testpoint.y = 48;
CATCH(errctx, akgl_collide_point_rectangle(&testpoint, &testrectpoints, &testcollide));
if ( testcollide == true ) {
FAIL_BREAK(errctx, AKGL_ERR_BEHAVIOR, "Invalid collision reported");
}
} CLEANUP {
} PROCESS(errctx) {
} FINISH(errctx, true);
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
{
SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
@@ -256,12 +90,12 @@ akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
* corner tests all report false. That was documented on akgl_collide_rectangles
* as a known limitation rather than fixed, and there was no test for it.
*
* The three cases below the cross are the ones a rewrite can break while fixing
* it: touching edges must keep counting as a collision, because
* akgl_collide_point_rectangle is inclusive on all four edges and both headers
* promise it; full containment must keep working; and a separation of less than
* one pixel must be seen, which the old implementation could not do because it
* routed through akgl_Point and truncated float to int.
* The three cases below the cross are the ones the rewrite could have broken
* while fixing it: touching edges must keep counting as a collision, because the
* corner form was inclusive on all four edges and the header promised it; full
* containment must keep working; and a separation of less than one pixel must be
* seen, which the old implementation could not do because it routed through the
* since-removed akgl_Point and truncated float to int.
*/
akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void)
{
@@ -295,8 +129,7 @@ akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void)
CATCH(errctx, akgl_collide_rectangles(&inner, &outer, &collide));
TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed");
// A shared edge and nothing more. akgl_collide_point_rectangle is
// inclusive on all four edges, so touching counts; util.h and chapter 18
// A shared edge and nothing more. Touching counts; util.h and chapter 19
// both say so, and a span test written with < rather than <= silently
// changes that.
CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide));
@@ -540,12 +373,6 @@ int main(void)
PREPARE_ERROR(errctx);
ATTEMPT {
CATCH(errctx, akgl_error_init());
CATCH(errctx, test_akgl_rectangle_points_nullpointers());
CATCH(errctx, test_akgl_rectangle_points_math());
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
// Defined since forever and never called until 0.5.0. TODO.md, "Known
// and still open" item 9.
CATCH(errctx, test_akgl_collide_point_rectangle_logic());
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
CATCH(errctx, test_akgl_collide_rectangles_logic());
CATCH(errctx, test_akgl_collide_rectangles_arrangements());