diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 78e76a0..f287815 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -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 diff --git a/TODO.md b/TODO.md index 1bed944..2bd22a2 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/docs/01-introduction.md b/docs/01-introduction.md index 6bb6081..728e0c4 100644 --- a/docs/01-introduction.md +++ b/docs/01-introduction.md @@ -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 diff --git a/docs/02-design-philosophy.md b/docs/02-design-philosophy.md index 58e09c5..f5f3b63 100644 --- a/docs/02-design-philosophy.md +++ b/docs/02-design-philosophy.md @@ -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. diff --git a/docs/04-errors.md b/docs/04-errors.md index cd434ae..451a5f0 100644 --- a/docs/04-errors.md +++ b/docs/04-errors.md @@ -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 diff --git a/docs/19-utilities.md b/docs/19-utilities.md index 1c8af27..a75df9e 100644 --- a/docs/19-utilities.md +++ b/docs/19-utilities.md @@ -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 diff --git a/docs/22-appendix-limits.md b/docs/22-appendix-limits.md index c0e7520..a996d61 100644 --- a/docs/22-appendix-limits.md +++ b/docs/22-appendix-limits.md @@ -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` | diff --git a/include/akgl/util.h b/include/akgl/util.h index 64c3e12..fdad551 100644 --- a/include/akgl/util.h +++ b/include/akgl/util.h @@ -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 #include -/** @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); diff --git a/src/util.c b/src/util.c index c045ef2..1334658 100644 --- a/src/util.c +++ b/src/util.c @@ -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)) && diff --git a/tests/perf.c b/tests/perf.c index 29907c0..462152e 100644 --- a/tests/perf.c +++ b/tests/perf.c @@ -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); diff --git a/tests/util.c b/tests/util.c index 515b4a7..ea3af40 100644 --- a/tests/util.c +++ b/tests/util.c @@ -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());