Report the overlap akgl_collide_rectangles could not see

It asked whether either rectangle enclosed one of the other's four corners --
eight akgl_collide_point_rectangle calls, stopping at the first hit. That is a
different question from "do these overlap", and it has the wrong answer for one
arrangement: a tall thin rectangle crossing a short wide one overlaps in a plus
sign with no corner of either inside the other, and all eight tests said no.

A long thin platform crossing a tall thin character is exactly that shape, so
this is a shape a 2D game produces, not a curiosity. util.h carried an @note
describing it and docs/18-utilities.md had a diagram of it, both under the
heading of a limitation rather than a defect, and there was no test for it at
all -- nor for full containment, nor for a shared edge.

It is four comparisons now, on both axes. `<=` rather than `<` because
akgl_collide_point_rectangle is inclusive on all four edges and these two have
always agreed that touching counts; a span test written with `<` would have
silently changed a contract both the header and the manual state.

The test was written first and failed on the cross before the fix went in.

Two answers change for a caller upgrading, and both are in the header note and
the chapter:

- The cross reports `true`, which is the point.
- The comparison is in float rather than through akgl_Point's int members, so a
  sub-pixel overlap is no longer truncated away. A pickup test that was
  accidentally forgiving by up to a pixel is no longer forgiving. Both tutorials
  use this for coins and hazards; both still pass.

Faster as a side effect rather than a goal, and worth recording because the
numbers move a documented budget: 24.9 ns -> 6.1 overlapping, 57.9 -> 6.1
disjoint, and the all-pairs sweep over 64 actors 115 us -> 12.2. The disjoint
case gained most because it was the one that ran all eight tests before
answering.

The three moved rows are re-recorded in PERFORMANCE.md and nothing else is.
akgl_rectangle_points is untouched by this change and reads 6.1 in the same run
against the 4.0 recorded, so 6 ns is this run's floor and the new figure means
"too cheap to measure" rather than "exactly 6.1" -- said in the prose so the
next reader does not re-baseline the table around it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:12:01 -04:00
parent 943bf4324e
commit 842ef75ddf
6 changed files with 158 additions and 75 deletions

View File

@@ -126,9 +126,9 @@ reciprocal.
| `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 | 24.9 | 40,181,108 |
| `akgl_collide_rectangles`, disjoint | call | 57.9 | 17,261,906 |
| all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 115,023.6 | 8,694 |
| `akgl_collide_rectangles`, overlapping | call | 6.1 | 164,345,776 |
| `akgl_collide_rectangles`, disjoint | call | 6.1 | 164,423,708 |
| all-pairs collision sweep, 64 actors (2016 pairs) | sweep | 12,241.6 | 81,688 |
| `akgl_string_initialize` | call | 32.1 | 31,197,432 |
| `akgl_string_copy`, full length | call | 32.2 | 31,075,004 |
| `json_load_file`, small document | load | 11,338.8 | 88,193 |
@@ -183,7 +183,7 @@ full screen of 16-pixel tiles and 64 actors:
| Part of the frame | Cost | Share of 16.67 ms |
|---|---:|---:|
| Logic: 64 actor updates + one physics sweep | 0.006 ms | 0.03% |
| All-pairs collision over 64 actors, if you do it | 0.115 ms | 0.7% |
| All-pairs collision over 64 actors, if you do it | 0.012 ms | 0.07% |
| Clear + present | 0.023 ms | 0.1% |
| 64 actor renders (48x48 blits) | 0.191 ms | 1.1% |
| 1200 tile blits | 16.26 ms | 97.6% |
@@ -306,15 +306,25 @@ and not a string operation.
### The collision helpers are fine; the missing broad phase is the problem
`akgl_collide_rectangles` is 24.9 ns when the rectangles overlap (it returns at
the first corner that hits) and 57.9 ns when they do not (all eight corner tests
run). Both are fine.
`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.
**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.
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, 115 µs a frame,
0.7% of a 60 fps budget. That is affordable. It is also O(n²): raise
`AKGL_MAX_HEAP_ACTOR` to 256 and the same loop is 32,640 pairs and 1.9 ms — over
10% of the frame, for a game that has done nothing yet.
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.
### Spawning is cheap, and a third of it is a log line