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

@@ -125,7 +125,6 @@ reciprocal.
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
| logic frame: 64 updates + simulate | frame | 5,763.1 | 173,517 |
| `akgl_rectangle_points` | call | 4.0 | 248,412,026 |
| `akgl_collide_rectangles`, overlapping | call | 6.1 | 164,345,776 |
| `akgl_collide_rectangles`, disjoint | call | 6.1 | 164,423,708 |
| control: all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 11,769.1 | 84,968 |
@@ -381,27 +380,29 @@ neither is the feature doing its job:
What is left is the struct, and it is paid whether or not a game uses collision.
That is worth knowing rather than smoothing over.
### The collision helpers are fine; the missing broad phase is the problem
### The overlap test is fine; the all-pairs loop around it was the problem
`akgl_collide_rectangles` is 6.1 ns whether the rectangles overlap or not. It
was 24.9 ns overlapping and 57.9 ns disjoint until 0.8.0, when it stopped being
eight `akgl_collide_point_rectangle` calls and became four comparisons -- a
change made to fix the cross case it could not see, with the speed as a side
effect rather than the goal. The disjoint case improved most because it was the
one that ran all eight tests before answering.
`akgl_collide_rectangles` is 6.1 ns whether the rectangles overlap or not. It was
24.9 ns overlapping and 57.9 ns disjoint until 0.8.0, when it stopped being eight
corner-containment tests and became four span comparisons -- a change made to fix
the cross case it could not see, with the speed as a side effect rather than the
goal. The disjoint case improved most because it was the one that ran all eight
tests before answering.
**Read that 6.1 as a floor, not a measurement.** `akgl_rectangle_points` is
untouched by that change and reads 6.1 in the same run against the 4.0 recorded
above, so this run's resolution is around 6 ns and the collision test is now too
cheap to distinguish from a function that does almost nothing. The rows above are
not re-baselined for it; only the three rows the change actually moved are.
**Read that 6.1 as a floor, not a measurement.** It is within this run's
resolution of a function that does almost nothing, and the rows above are not
re-baselined for it; only the rows the change actually moved are.
What the library does not provide is a broad phase, so a caller that wants
collision writes the all-pairs loop: 2016 pairs for 64 actors, **12 µs** a frame,
0.07% of a 60 fps budget. That is affordable. It is still O(n²): raise
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs, which at this
per-pair cost is around 200 µs — 1.2% of the frame, for a game that has done
nothing yet.
The cost was never the test. It was the loop a caller had to write around it,
because there was no broad phase: 2016 pairs for 64 actors, **12 µs** a frame,
0.07% of a 60 fps budget. Affordable, and still O(n²) -- at
`AKGL_MAX_HEAP_ACTOR=256` the same loop is 32,640 pairs and **188.5 µs measured**,
1.1% of the frame for a game that has done nothing yet.
**That loop is what 0.8.0 removed the need for.** A whole physics step with a
collision world attached is 54.1 µs at 256 actors, doing strictly more -- see
"Collision, measured" below. The all-pairs row stays as the `control:` it is
labelled, which is what it is measuring now.
### Spawning is cheap, and a third of it is a log line

46
TODO.md
View File

@@ -2518,36 +2518,36 @@ follow that are true today and would not survive a change:
`akgl_ccd_arena_highwater()` is public so a game can assert its own ceiling.
## The `util.h` rectangle helpers stay, and why
## `akgl_collide_rectangles` stays; the corner helpers did not
The collision plan's last step was to **delete `akgl_rectangle_points`,
`akgl_collide_point_rectangle` and `akgl_collide_rectangles`** once the shape API
landed, on the grounds that collision now has a real narrowphase. That step was
not taken, and this is the reasoning rather than an oversight.
landed, on the grounds that collision now has a real narrowphase. Two of the
three went, in 0.8.0, along with `akgl_Point` and `akgl_RectanglePoints`. The
third stays, and this is the reasoning rather than an oversight.
- **They still have callers that are correct.** `examples/sidescroller/player.c`
uses `akgl_collide_rectangles` twice, for coins and for hazards. Those are
overlap *questions* asked from an `updatefunc` -- "am I touching this" -- and
the right answer is a `bool`. Routing them through a collision shape would
mean a proxy, a broad-phase insert and a narrowphase call to compute a normal
and a depth that nothing reads.
- **`akgl_collide_rectangles` was fixed in this same series**, not left broken.
It was a corner test that missed the cross case and truncated through
`akgl_Point`'s `int` members; it is a span test now, exact in `float32_t`.
Deleting it two commits after fixing it is a strange thing to do to a caller.
**Why the two went.** They were the intermediate form of an implementation that
changed. `akgl_collide_rectangles` was eight corner-containment tests built on
them; it is four span comparisons now and does not go through either. Nothing
outside `tests/` called them, and a point-in-rectangle test is four comparisons a
caller can write without a struct conversion in front of it.
**Why the third stays.**
- **It has callers that are correct.** `examples/sidescroller/player.c` uses it
twice, for coins and for hazards. Those are overlap *questions* asked from an
`updatefunc` -- "am I touching this" -- and the right answer is a `bool`.
Routing them through a collision shape would mean a proxy, a broad-phase insert
and a narrowphase call to compute a normal and a depth that nothing reads.
- **It was fixed in this same series**, not left broken. It missed the cross case
and truncated through `akgl_Point`'s `int` members; it is exact in `float32_t`
now. Deleting it two commits after fixing it is a strange thing to do to a
caller.
- **A game-level overlap question is not always a physics question.** A minimap
marker, a UI hit test, "is the cursor over this". None of those wants to be
pushed, and none of them belongs in a collision world.
`akgl_rectangle_points` and `akgl_collide_point_rectangle` are the weaker case:
nothing outside `tests/` calls either, and `akgl_collide_rectangles` no longer
does since it stopped working corner by corner. They are kept for symmetry with
the one that is used and because a point-in-rectangle test is the other half of
the same question -- but if a 0.9.0 wants to trim the public surface, those two
are the candidates, not `akgl_collide_rectangles`.
One thing found while writing this and fixed rather than recorded:
One thing found while doing this and fixed rather than recorded:
`akgl_RectanglePoints`' Doxygen comment still said `akgl_collide_rectangles`
"works corner by corner rather than by comparing edge spans", which had been
false since the fix. [Chapter 19](docs/19-utilities.md) already described the
current behaviour correctly, so the header was the only place saying otherwise.
false since the fix. It went with the type.

View File

@@ -1,13 +1,15 @@
# 01. Introduction
libakgl is a C library for building 2D games on SDL3. This is version **0.8.0**. It ships
195 functions declared across the twenty-three public headers in `include/akgl/`, plus
`akgl_version()` from the generated `version.h` — 196 exported `akgl_` symbols in
193 functions declared across the twenty-three public headers in `include/akgl/`, plus
`akgl_version()` from the generated `version.h` — 194 exported `akgl_` symbols in
`libakgl.so.0.8`.
**0.8.0 is an ABI break.** The `collide` slot on `akgl_PhysicsBackend` changed signature,
four public structs grew, and `akgl_physics_arcade_collide` stopped raising `AKERR_API`.
Collision is the reason; it is [Chapter 15](15-collision.md).
four public structs grew, `akgl_physics_arcade_collide` stopped raising `AKERR_API`, and
four symbols were removed — `akgl_rectangle_points`, `akgl_collide_point_rectangle` and the
two types they used ([Chapter 19](19-utilities.md)). Collision is the reason for all of it;
it is [Chapter 15](15-collision.md).
It gives you object pools instead of `malloc`, name-based registries instead of pointer
plumbing, a per-frame tick that updates and draws every live actor, JSON asset formats for
@@ -122,7 +124,7 @@ text you read *is* the header.
## Reading order
[Chapter 4](04-errors.md) comes before every subsystem chapter, because 183 of those 195
[Chapter 4](04-errors.md) comes before every subsystem chapter, because 181 of those 193
functions return `akerr_ErrorContext AKERR_NOIGNORE *` and you cannot read a single example
until you can read that return value. The dozen that do not are the `void` SDL enumeration
callbacks and the collision arena's accessors. After that, [Chapter 3](03-getting-started.md) gets a window on

View File

@@ -214,7 +214,7 @@ in the tree negated it, which is the only reason that was latent rather than liv
## Errors carry context, and callers cannot ignore them
**183 of libakgl's 195 functions return `akerr_ErrorContext AKERR_NOIGNORE *`.**
**181 of libakgl's 193 functions return `akerr_ErrorContext AKERR_NOIGNORE *`.**
Results come back through pointer parameters; the return value is always the error. The
`AKERR_NOIGNORE` attribute makes discarding it a compiler diagnostic.

View File

@@ -14,7 +14,7 @@ This manual does not restate them, because a copy here would be wrong the day li
changes and nothing in this repository would notice.
What is genuinely libakgl's, and is written down nowhere else, is **which statuses these
195 functions raise and what each one means here**. That is the three tables below.
193 functions raise and what each one means here**. That is the three tables below.
The house rules for *writing* code against the protocol — never a `*_RETURN` inside an
`ATTEMPT`, never a bare `return` out of a `HANDLE`, `CLEANUP` before `PROCESS`, an

View File

@@ -6,14 +6,12 @@ everything else passes around. The JSON accessors are the valuable part of this
their status semantics are what every asset loader in the library is written against, and
they are documented nowhere else.
## Rectangle overlap helpers
## The rectangle overlap test
Three functions, all axis-aligned, all in `util.h`:
One function, in `util.h`:
| Function | Question it answers |
|---|---|
| `akgl_rectangle_points(dest, rect)` | What are the four corners of this `SDL_FRect`? |
| `akgl_collide_point_rectangle(p, r, collide)` | Is this point inside those corners? |
| `akgl_collide_rectangles(r1, r2, collide)` | Do these two rectangles overlap? |
```c
@@ -28,38 +26,19 @@ akerr_ErrorContext *boxes_touch(SDL_FRect *a, SDL_FRect *b, bool *hit)
PASS(errctx, akgl_collide_rectangles(a, b, hit));
SUCCEED_RETURN(errctx);
}
/* Is the mouse over a button? Corners first, then the point test. */
akerr_ErrorContext *point_in_box(int x, int y, SDL_FRect *box, bool *hit)
{
akgl_RectanglePoints corners;
akgl_Point p;
PREPARE_ERROR(errctx);
p.x = x;
p.y = y;
p.z = 0;
PASS(errctx, akgl_rectangle_points(&corners, box));
PASS(errctx, akgl_collide_point_rectangle(&p, &corners, hit));
SUCCEED_RETURN(errctx);
}
```
Four properties, all deliberate:
Three properties, all deliberate:
**Axis-aligned only.** There is no rotation support and no separating-axis test. An actor
drawn with a non-zero `angle` still collides as its unrotated box.
**Axis-aligned only.** There is no rotation support and no separating-axis test here. If you
want either, you want a collision shape — [Chapter 15](15-collision.md).
**Edges count as touching.** A point exactly on a boundary is inside; two rectangles sharing
an edge and no area overlap. Both comparisons are `>=` and `<=`.
**Edges count as touching.** Two rectangles sharing an edge and no area overlap. Every
comparison is `<=`, and a version written with `<` would silently change what every pickup
test in a consumer answers.
**`akgl_rectangle_points` truncates from `float` to `int`.** A rectangle at `x = 10.9` has
its corners at 10. That is right for tile-grid work and wrong for sub-pixel work; if you
need the latter, do not round-trip through it. **`akgl_collide_rectangles` no longer does**
— it compares the spans in `float` directly, so a sub-pixel overlap registers.
**`z` is carried and never used.** `akgl_Point` has one, `akgl_rectangle_points` never
writes it, and neither collision test reads it. These are 2D tests.
**The comparison is in `float32_t`.** No intermediate form, no truncation, so an overlap
smaller than a pixel registers.
### The arrangement it used to miss
@@ -84,21 +63,34 @@ tall thin character is exactly that shape. The header carried a `@note` describi
than a fix.
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
`<=` rather than `<` because [`akgl_collide_point_rectangle`](#rectangle-overlap-helpers) is
inclusive on all four edges and these two have always agreed: touching counts.
**If you are upgrading from 0.7.x, two answers change.** The cross now reports `true`, which
is the point. And because the comparison no longer round-trips through `akgl_Point`'s `int`
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
**If you are upgrading from 0.7.x, three things change.** The cross now reports `true`, which
is the point. The comparison no longer round-trips through an `int` intermediate, so overlaps
and gaps smaller than a pixel are seen rather than truncated away — a pickup test that was
accidentally forgiving by up to a pixel is no longer forgiving. And **four symbols are
gone**:
**The library runs its own collision now** — see [Chapter 15](15-collision.md) — and it does
not use these. `akgl_collision_test` works in centres and half-extents, answers with a normal
and a depth rather than a boolean, and handles circles and capsules as well as boxes.
| Removed in 0.8.0 | Why | What to write instead |
|---|---|---|
| `akgl_rectangle_points` | Built the corner form `akgl_collide_rectangles` no longer uses | Nothing. It had no other caller |
| `akgl_collide_point_rectangle` | Same | `p.x >= r.x && p.x <= r.x + r.w` on both axes — four comparisons, and no `SDL_FRect`-to-corners conversion in front of them |
| `akgl_Point` | Existed to carry an `int` position into the above, with a `z` nothing ever read | `SDL_FPoint`, if you want a point type |
| `akgl_RectanglePoints` | Existed to carry four of them | — |
These stay because a game-level overlap question is not always a physics question: a
minimap marker, a UI hit test, "is the cursor over this". For anything that should push or be
pushed, use a collision shape.
Nothing outside `tests/` called either function. They were the intermediate form of an
implementation that changed.
### What stays, and why it is not collision
**The library runs its own collision** — [Chapter 15](15-collision.md) — and it does not use
this. `akgl_collision_test` works in centres and half-extents, answers with a normal and a
depth rather than a boolean, and handles circles and capsules as well as boxes.
`akgl_collide_rectangles` stays because a game-level overlap question is not always a physics
question: a coin, a minimap marker, a UI hit test, "is the cursor over this". Both tutorials
use it for pickups and hazards from an `updatefunc`, where a `bool` is the whole answer and a
proxy, a broad-phase insert and a narrowphase call would be computing a normal nothing reads.
For anything that should push or be pushed, use a collision shape.
## Path resolution

View File

@@ -188,7 +188,7 @@ What the statuses mean is [Chapter 4](04-errors.md).
| `akgl_audio_voice_active`, `akgl_audio_mix` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` |
| `akgl_load_start_bgm` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
| `akgl_path_relative` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` |
| `akgl_rectangle_points`, `akgl_collide_point_rectangle`, `akgl_collide_rectangles` | `AKERR_NULLPOINTER` |
| `akgl_collide_rectangles` | `AKERR_NULLPOINTER` |
| `akgl_compare_sdl_surfaces` | `AKERR_NULLPOINTER`, `AKERR_VALUE` |
| `akgl_render_and_compare` | `AKERR_NULLPOINTER`, `AKERR_IO`, `AKGL_ERR_SDL` |
| `akgl_string_initialize` | `AKERR_NULLPOINTER` |

View File

@@ -1,15 +1,16 @@
/**
* @file util.h
* @brief Axis-aligned collision tests, path resolution, and two test-only image helpers.
* @brief A rectangle overlap test, 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.
* The grab bag. Three unrelated groups live here: one rectangle overlap test,
* 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.
* akgl_collide_rectangles is axis-aligned and treats edges as touching: two
* rectangles sharing an edge and nothing more overlap. It answers a *game's*
* question -- a pickup, a minimap marker, a UI hit test -- and is deliberately
* not what the physics step uses. Anything that should push or be pushed wants a
* collision shape and akgl_collision_test; see `akgl/collision.h`.
*/
#ifndef _AKGL_UTIL_H_
@@ -20,71 +21,12 @@
#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.
*
* The form akgl_collide_point_rectangle wants. akgl_rectangle_points derives one
* of these from an `SDL_FRect`.
*
* @note akgl_collide_rectangles used to be built on this, corner by corner, and
* missed the cross case that way -- two rectangles overlapping in a plus
* sign have no corner of either inside the other. It compares edge spans
* in `float32_t` since 0.8.0 and does not go through here at all.
*/
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.
* Compares the two rectangles' spans on both axes: they overlap when neither is
* wholly to one side of the other. Four comparisons, in `float32_t`, with no
* intermediate form.
*
* @param r1 First rectangle. Required.
* @param r2 Second rectangle. Required. Order does not matter.
@@ -99,8 +41,15 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_point_rectangle(akgl_Point *p, a
* the other, and every corner test said no. It is a span comparison on
* both axes now. Two consequences for a caller upgrading: that
* arrangement starts reporting `true`, and the comparison is in `float`
* rather than through akgl_Point's `int` members, so an overlap smaller
* than one pixel is no longer truncated away.
* rather than through the removed akgl_Point's `int` members, so an
* overlap smaller than one pixel is no longer truncated away.
*
* @note akgl_rectangle_points, akgl_collide_point_rectangle, akgl_Point and
* akgl_RectanglePoints were removed in 0.8.0. They existed to feed the
* corner form this no longer uses, and nothing outside `tests/` called
* either function. A point-in-rectangle test is four comparisons a caller
* can write; the intermediate struct was the only thing this header was
* adding.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);

View File

@@ -130,37 +130,6 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *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);
@@ -177,13 +146,14 @@ akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *
* 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.
* `<=` and not `<`: two rectangles sharing exactly one edge collide. That is
* what the corner form did, inclusive on all four edges, and changing it
* would silently change what every pickup test in a consumer answers.
*
* 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.
* whose members were int, so a rectangle at x = 10.9 had its corner at 10
* and a sub-pixel overlap was invisible. That type is gone with the corner
* functions, in 0.8.0.
*/
*collide = ( (r1->x <= (r2->x + r2->w)) &&
(r2->x <= (r1->x + r1->w)) &&

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());