Remove the corner helpers akgl_collide_rectangles no longer uses
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:
@@ -125,7 +125,6 @@ reciprocal.
|
|||||||
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
|
| `akgl_physics_simulate`, 64 live actors | frame | 1,216.8 | 821,841 |
|
||||||
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
|
| `akgl_physics_simulate`, empty pool | frame | 63.9 | 15,650,101 |
|
||||||
| logic frame: 64 updates + simulate | frame | 5,763.1 | 173,517 |
|
| 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`, overlapping | call | 6.1 | 164,345,776 |
|
||||||
| `akgl_collide_rectangles`, disjoint | call | 6.1 | 164,423,708 |
|
| `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 |
|
| 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.
|
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.
|
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
|
`akgl_collide_rectangles` is 6.1 ns whether the rectangles overlap or not. It was
|
||||||
was 24.9 ns overlapping and 57.9 ns disjoint until 0.8.0, when it stopped being
|
24.9 ns overlapping and 57.9 ns disjoint until 0.8.0, when it stopped being eight
|
||||||
eight `akgl_collide_point_rectangle` calls and became four comparisons -- a
|
corner-containment tests and became four span comparisons -- a change made to fix
|
||||||
change made to fix the cross case it could not see, with the speed as a side
|
the cross case it could not see, with the speed as a side effect rather than the
|
||||||
effect rather than the goal. The disjoint case improved most because it was the
|
goal. The disjoint case improved most because it was the one that ran all eight
|
||||||
one that ran all eight tests before answering.
|
tests before answering.
|
||||||
|
|
||||||
**Read that 6.1 as a floor, not a measurement.** `akgl_rectangle_points` is
|
**Read that 6.1 as a floor, not a measurement.** It is within this run's
|
||||||
untouched by that change and reads 6.1 in the same run against the 4.0 recorded
|
resolution of a function that does almost nothing, and the rows above are not
|
||||||
above, so this run's resolution is around 6 ns and the collision test is now too
|
re-baselined for it; only the rows the change actually moved are.
|
||||||
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.
|
|
||||||
|
|
||||||
What the library does not provide is a broad phase, so a caller that wants
|
The cost was never the test. It was the loop a caller had to write around it,
|
||||||
collision writes the all-pairs loop: 2016 pairs for 64 actors, **12 µs** a frame,
|
because there was no broad phase: 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
|
0.07% of a 60 fps budget. Affordable, and still O(n²) -- at
|
||||||
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs, which at this
|
`AKGL_MAX_HEAP_ACTOR=256` the same loop is 32,640 pairs and **188.5 µs measured**,
|
||||||
per-pair cost is around 200 µs — 1.2% of the frame, for a game that has done
|
1.1% of the frame for a game that has done nothing yet.
|
||||||
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
|
### Spawning is cheap, and a third of it is a log line
|
||||||
|
|
||||||
|
|||||||
46
TODO.md
46
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.
|
`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`,
|
The collision plan's last step was to **delete `akgl_rectangle_points`,
|
||||||
`akgl_collide_point_rectangle` and `akgl_collide_rectangles`** once the shape API
|
`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
|
landed, on the grounds that collision now has a real narrowphase. Two of the
|
||||||
not taken, and this is the reasoning rather than an oversight.
|
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`
|
**Why the two went.** They were the intermediate form of an implementation that
|
||||||
uses `akgl_collide_rectangles` twice, for coins and for hazards. Those are
|
changed. `akgl_collide_rectangles` was eight corner-containment tests built on
|
||||||
overlap *questions* asked from an `updatefunc` -- "am I touching this" -- and
|
them; it is four span comparisons now and does not go through either. Nothing
|
||||||
the right answer is a `bool`. Routing them through a collision shape would
|
outside `tests/` called them, and a point-in-rectangle test is four comparisons a
|
||||||
mean a proxy, a broad-phase insert and a narrowphase call to compute a normal
|
caller can write without a struct conversion in front of it.
|
||||||
and a depth that nothing reads.
|
|
||||||
- **`akgl_collide_rectangles` was fixed in this same series**, not left broken.
|
**Why the third stays.**
|
||||||
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`.
|
- **It has callers that are correct.** `examples/sidescroller/player.c` uses it
|
||||||
Deleting it two commits after fixing it is a strange thing to do to a caller.
|
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
|
- **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
|
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.
|
pushed, and none of them belongs in a collision world.
|
||||||
|
|
||||||
`akgl_rectangle_points` and `akgl_collide_point_rectangle` are the weaker case:
|
One thing found while doing this and fixed rather than recorded:
|
||||||
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:
|
|
||||||
`akgl_RectanglePoints`' Doxygen comment still said `akgl_collide_rectangles`
|
`akgl_RectanglePoints`' Doxygen comment still said `akgl_collide_rectangles`
|
||||||
"works corner by corner rather than by comparing edge spans", which had been
|
"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
|
false since the fix. It went with the type.
|
||||||
current behaviour correctly, so the header was the only place saying otherwise.
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
# 01. Introduction
|
# 01. Introduction
|
||||||
|
|
||||||
libakgl is a C library for building 2D games on SDL3. This is version **0.8.0**. It ships
|
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
|
193 functions declared across the twenty-three public headers in `include/akgl/`, plus
|
||||||
`akgl_version()` from the generated `version.h` — 196 exported `akgl_` symbols in
|
`akgl_version()` from the generated `version.h` — 194 exported `akgl_` symbols in
|
||||||
`libakgl.so.0.8`.
|
`libakgl.so.0.8`.
|
||||||
|
|
||||||
**0.8.0 is an ABI break.** The `collide` slot on `akgl_PhysicsBackend` changed signature,
|
**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`.
|
four public structs grew, `akgl_physics_arcade_collide` stopped raising `AKERR_API`, and
|
||||||
Collision is the reason; it is [Chapter 15](15-collision.md).
|
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
|
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
|
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
|
## 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
|
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
|
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
|
callbacks and the collision arena's accessors. After that, [Chapter 3](03-getting-started.md) gets a window on
|
||||||
|
|||||||
@@ -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
|
## 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
|
Results come back through pointer parameters; the return value is always the error. The
|
||||||
`AKERR_NOIGNORE` attribute makes discarding it a compiler diagnostic.
|
`AKERR_NOIGNORE` attribute makes discarding it a compiler diagnostic.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
changes and nothing in this repository would notice.
|
||||||
|
|
||||||
What is genuinely libakgl's, and is written down nowhere else, is **which statuses these
|
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
|
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
|
`ATTEMPT`, never a bare `return` out of a `HANDLE`, `CLEANUP` before `PROCESS`, an
|
||||||
|
|||||||
@@ -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
|
their status semantics are what every asset loader in the library is written against, and
|
||||||
they are documented nowhere else.
|
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 |
|
| 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? |
|
| `akgl_collide_rectangles(r1, r2, collide)` | Do these two rectangles overlap? |
|
||||||
|
|
||||||
```c
|
```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));
|
PASS(errctx, akgl_collide_rectangles(a, b, hit));
|
||||||
SUCCEED_RETURN(errctx);
|
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
|
**Axis-aligned only.** There is no rotation support and no separating-axis test here. If you
|
||||||
drawn with a non-zero `angle` still collides as its unrotated box.
|
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
|
**Edges count as touching.** Two rectangles sharing an edge and no area overlap. Every
|
||||||
an edge and no area overlap. Both comparisons are `>=` and `<=`.
|
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
|
**The comparison is in `float32_t`.** No intermediate form, no truncation, so an overlap
|
||||||
its corners at 10. That is right for tile-grid work and wrong for sub-pixel work; if you
|
smaller than a pixel registers.
|
||||||
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 arrangement it used to miss
|
### 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.
|
than a fix.
|
||||||
|
|
||||||
It is four comparisons now, `r1.x <= r2.x + r2.w && r2.x <= r1.x + r1.w` on both axes.
|
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
|
**If you are upgrading from 0.7.x, three things change.** The cross now reports `true`, which
|
||||||
is the point. And because the comparison no longer round-trips through `akgl_Point`'s `int`
|
is the point. The comparison no longer round-trips through an `int` intermediate, so overlaps
|
||||||
members, overlaps and gaps smaller than a pixel are now seen rather than truncated away — so
|
and gaps smaller than a pixel are seen rather than truncated away — a pickup test that was
|
||||||
a pickup test that was accidentally forgiving by up to a pixel is no longer forgiving.
|
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
|
| Removed in 0.8.0 | Why | What to write instead |
|
||||||
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.
|
| `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
|
Nothing outside `tests/` called either function. They were the intermediate form of an
|
||||||
minimap marker, a UI hit test, "is the cursor over this". For anything that should push or be
|
implementation that changed.
|
||||||
pushed, use a collision shape.
|
|
||||||
|
### 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
|
## Path resolution
|
||||||
|
|
||||||
|
|||||||
@@ -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_audio_voice_active`, `akgl_audio_mix` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` |
|
||||||
| `akgl_load_start_bgm` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
|
| `akgl_load_start_bgm` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
|
||||||
| `akgl_path_relative` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` |
|
| `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_compare_sdl_surfaces` | `AKERR_NULLPOINTER`, `AKERR_VALUE` |
|
||||||
| `akgl_render_and_compare` | `AKERR_NULLPOINTER`, `AKERR_IO`, `AKGL_ERR_SDL` |
|
| `akgl_render_and_compare` | `AKERR_NULLPOINTER`, `AKERR_IO`, `AKGL_ERR_SDL` |
|
||||||
| `akgl_string_initialize` | `AKERR_NULLPOINTER` |
|
| `akgl_string_initialize` | `AKERR_NULLPOINTER` |
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* @file util.h
|
* @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 grab bag. Three unrelated groups live here: one rectangle overlap test,
|
||||||
* the physics backend, path resolution for the asset loaders, and a pair of
|
* path resolution for the asset loaders, and a pair of pixel-comparison routines
|
||||||
* pixel-comparison routines that exist only so tests can assert on what was
|
* that exist only so tests can assert on what was actually drawn.
|
||||||
* actually drawn.
|
|
||||||
*
|
*
|
||||||
* All the geometry here is axis-aligned and treats edges as touching: a point
|
* akgl_collide_rectangles is axis-aligned and treats edges as touching: two
|
||||||
* exactly on a boundary is inside. There is no rotation support and no
|
* rectangles sharing an edge and nothing more overlap. It answers a *game's*
|
||||||
* separating-axis test.
|
* 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_
|
#ifndef _AKGL_UTIL_H_
|
||||||
@@ -20,71 +21,12 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <akgl/staticstring.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.
|
* @brief Test whether two rectangles overlap, edges included.
|
||||||
*
|
*
|
||||||
* Tests all eight corners -- each rectangle's four against the other -- and
|
* Compares the two rectangles' spans on both axes: they overlap when neither is
|
||||||
* stops at the first hit. Checking both directions is what catches the case
|
* wholly to one side of the other. Four comparisons, in `float32_t`, with no
|
||||||
* where one rectangle is entirely inside the other and so has no corner within
|
* intermediate form.
|
||||||
* its neighbour.
|
|
||||||
*
|
*
|
||||||
* @param r1 First rectangle. Required.
|
* @param r1 First rectangle. Required.
|
||||||
* @param r2 Second rectangle. Required. Order does not matter.
|
* @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
|
* the other, and every corner test said no. It is a span comparison on
|
||||||
* both axes now. Two consequences for a caller upgrading: that
|
* both axes now. Two consequences for a caller upgrading: that
|
||||||
* arrangement starts reporting `true`, and the comparison is in `float`
|
* arrangement starts reporting `true`, and the comparison is in `float`
|
||||||
* rather than through akgl_Point's `int` members, so an overlap smaller
|
* rather than through the removed akgl_Point's `int` members, so an
|
||||||
* than one pixel is no longer truncated away.
|
* 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);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide);
|
||||||
|
|
||||||
|
|||||||
42
src/util.c
42
src/util.c
@@ -130,37 +130,6 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dest
|
|||||||
SUCCEED_RETURN(errctx);
|
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)
|
akerr_ErrorContext *akgl_collide_rectangles(SDL_FRect *r1, SDL_FRect *r2, bool *collide)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
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
|
* all eight tests said no. util.h carried a @note describing the arrangement
|
||||||
* rather than a fix.
|
* rather than a fix.
|
||||||
*
|
*
|
||||||
* `<=` and not `<`, because akgl_collide_point_rectangle is inclusive on all
|
* `<=` and not `<`: two rectangles sharing exactly one edge collide. That is
|
||||||
* four edges and this function has always agreed with it: two rectangles
|
* what the corner form did, inclusive on all four edges, and changing it
|
||||||
* sharing exactly one edge collide.
|
* would silently change what every pickup test in a consumer answers.
|
||||||
*
|
*
|
||||||
* The comparison is in float now. The corner form went through akgl_Point,
|
* 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
|
* whose members were int, so a rectangle at x = 10.9 had its corner at 10
|
||||||
* a sub-pixel overlap was invisible.
|
* 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)) &&
|
*collide = ( (r1->x <= (r2->x + r2->w)) &&
|
||||||
(r2->x <= (r1->x + r1->w)) &&
|
(r2->x <= (r1->x + r1->w)) &&
|
||||||
|
|||||||
26
tests/perf.c
26
tests/perf.c
@@ -758,14 +758,6 @@ static akerr_ErrorContext *bench_logic_frame(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
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. */
|
/** @brief Counts nothing; the query cost is what is being measured. */
|
||||||
static akerr_ErrorContext *bench_collision_visit(akgl_CollisionProxy *proxy, void *data)
|
static akerr_ErrorContext *bench_collision_visit(akgl_CollisionProxy *proxy, void *data)
|
||||||
{
|
{
|
||||||
@@ -895,11 +887,22 @@ static akerr_ErrorContext *bench_collision(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
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)
|
static akerr_ErrorContext *bench_geometry(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
akerr_ErrorContext *inner = NULL;
|
akerr_ErrorContext *inner = NULL;
|
||||||
akgl_RectanglePoints points;
|
|
||||||
SDL_FRect rects[BENCH_ACTOR_COUNT];
|
SDL_FRect rects[BENCH_ACTOR_COUNT];
|
||||||
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
|
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 };
|
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;
|
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
|
||||||
|
|
||||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
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_start("collide_rectangles, overlapping", "call", 300.0);
|
||||||
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
|
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
|
||||||
bench_stop(count);
|
bench_stop(count);
|
||||||
|
|||||||
187
tests/util.c
187
tests/util.c
@@ -28,172 +28,6 @@ static int live_error_contexts(void)
|
|||||||
return live;
|
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)
|
akerr_ErrorContext *test_akgl_collide_rectangles_nullpointers(void)
|
||||||
{
|
{
|
||||||
SDL_FRect testrect1 = {.x = 0, .y = 0, .w = 0, .h = 0};
|
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
|
* 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.
|
* 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
|
* The three cases below the cross are the ones the rewrite could have broken
|
||||||
* it: touching edges must keep counting as a collision, because
|
* while fixing it: touching edges must keep counting as a collision, because the
|
||||||
* akgl_collide_point_rectangle is inclusive on all four edges and both headers
|
* corner form was inclusive on all four edges and the header promised it; full
|
||||||
* promise it; full containment must keep working; and a separation of less than
|
* containment must keep working; and a separation of less than one pixel must be
|
||||||
* one pixel must be seen, which the old implementation could not do because it
|
* seen, which the old implementation could not do because it routed through the
|
||||||
* routed through akgl_Point and truncated float to int.
|
* since-removed akgl_Point and truncated float to int.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext *test_akgl_collide_rectangles_arrangements(void)
|
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));
|
CATCH(errctx, akgl_collide_rectangles(&inner, &outer, &collide));
|
||||||
TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed");
|
TEST_ASSERT(errctx, (collide == true), "a containing rectangle was missed");
|
||||||
|
|
||||||
// A shared edge and nothing more. akgl_collide_point_rectangle is
|
// A shared edge and nothing more. Touching counts; util.h and chapter 19
|
||||||
// inclusive on all four edges, so touching counts; util.h and chapter 18
|
|
||||||
// both say so, and a span test written with < rather than <= silently
|
// both say so, and a span test written with < rather than <= silently
|
||||||
// changes that.
|
// changes that.
|
||||||
CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide));
|
CATCH(errctx, akgl_collide_rectangles(&left, &right, &collide));
|
||||||
@@ -540,12 +373,6 @@ int main(void)
|
|||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_error_init());
|
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_nullpointers());
|
||||||
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
||||||
CATCH(errctx, test_akgl_collide_rectangles_arrangements());
|
CATCH(errctx, test_akgl_collide_rectangles_arrangements());
|
||||||
|
|||||||
Reference in New Issue
Block a user