Test the collision scan, and measure what it costs
`spr_collisions()` had no test. `tests/sprite_verbs.c` drives the collision path end to end but through a mock backend, so the real overlap arithmetic in `src/sprite_akgl.c` could have changed what `BUMP(1)` reports for every program in existence and the suite would still have printed 110/110. That gap is closed here, before anything touches the arithmetic, so that there is a *before* to compare an *after* against. Six cases against the real akgl backend: nothing defined, two sprites overlapping, edge-to-edge, a hidden sprite, and the x-expand bit doubling the box that collides rather than only the one that draws. Edge-to-edge earns its place -- the test is a strict `<`, a tile-aligned program puts sprites there constantly, and a replacement answering "touching" instead of "overlapping" would change every one of them silently. **The seventh is the cross-shaped overlap**, and it is the one to watch. A tall thin sprite crossing a short wide one overlaps without either rectangle holding a corner of the other; `akgl_collide_rectangles()` is documented as answering "no" there, which is why `src/sprite_akgl.c` does the four comparisons itself rather than calling it. Two further assertions stop that test passing by accident: each sprite is moved clear along the axis it is supposed to be short on, so a sprite that came out the wrong size fails rather than quietly reporting an ordinary overlap. `tests/collision_perf.c` answers the question nobody had measured. The service runs at the top of every interpreter *step* and the frontend takes 256 steps per rendered frame, so a busy program scans up to 256 times a frame over sprites that have not moved. At RelWithDebInfo, scale 10, best of 5: the scan is 96.3 ns at eight overlapping sprites and 19.4 ns at none, against a rendered frame of 1.17 ms. **256 scans is 24.7 us, or 2.1% of a frame, in the pathological case, and 0.42% for a program with no sprites.** So the per-step cadence stays. It is what makes a collision report describe where the sprites have just been moved to rather than where they were, and 2% of a frame in a case no real program reaches is not worth changing when a handler fires for every program that already works. The numbers and that conclusion are in `MAINTENANCE.md` so it does not get re-argued. The benchmark borrows libakgl's `benchutil.h` by include path rather than copying it, the way the fixture font is already borrowed, and is labelled `perf` so `ctest -LE perf` can leave it out. It runs at scale 1 in the ordinary suite -- 1.2 seconds -- because a benchmark nothing ever builds is a benchmark that rots. Both suites green: 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
This commit is contained in:
@@ -387,6 +387,53 @@ to more branches per call site — a denominator change, not a regression.
|
||||
`/home/andrew/local` and deletes build directories. Use the portable commands above unless
|
||||
that exact behaviour is what you want.
|
||||
|
||||
### Benchmarks, and what the collision scan actually costs
|
||||
|
||||
`tests/collision_perf.c` is the only benchmark here. It is labelled `perf`, so `ctest -LE perf`
|
||||
leaves it out of the ordinary run, and it borrows `deps/libakgl/tests/benchutil.h` by include
|
||||
path rather than copying it — the harness is the house convention across these repositories and
|
||||
a second copy would be a fork.
|
||||
|
||||
**Both checked-in build trees are `Debug`, and `-O0` numbers are not worth reading.**
|
||||
`benchutil.h` refuses to enforce a budget without `__OPTIMIZE__` for exactly that reason.
|
||||
Configure a third tree and run it at scale:
|
||||
|
||||
```sh norun
|
||||
cmake -S . -B build-perf -DAKBASIC_WITH_AKGL=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build-perf --target akbasic_test_collision_perf
|
||||
cd build-perf && AKGL_BENCH_SCALE=10 ctest -L perf
|
||||
```
|
||||
|
||||
The question it was written to settle: `akbasic_collision_service()` runs at the top of every
|
||||
interpreter **step**, and the akgl frontend takes `AKBASIC_FRONTEND_STEPS_PER_FRAME` (256) steps
|
||||
per rendered frame — so a busy program scans up to 256 times a frame, nearly always over sprites
|
||||
that have not moved. Is that worth changing to a per-frame cadence?
|
||||
|
||||
Measured at `RelWithDebInfo`, scale 10, best of 5:
|
||||
|
||||
| Row | ns/op |
|
||||
|---|---|
|
||||
| `spr_collisions`, 0 sprites | 19.4 |
|
||||
| `spr_collisions`, 2 sprites | 26.1 |
|
||||
| `spr_collisions`, 4 sprites | 40.3 |
|
||||
| `spr_collisions`, 8 sprites, all overlapping | 96.3 |
|
||||
| one rendered frame, 8 sprites + full text grid | 1,174,081 |
|
||||
|
||||
**The answer is no.** 256 scans at 96.3 ns is 24.7 µs against a 1.17 ms frame — **2.1%**, and
|
||||
that is the pathological case: eight sprites all overlapping, in a program that never blocks and
|
||||
therefore burns all 256 steps. The cost a program with no sprites pays is 0.42% of a frame. The
|
||||
frame is dominated by the text layer repainting every row it owns, which `TODO.md` already
|
||||
records as the real rendering cost.
|
||||
|
||||
So the per-step cadence stays. It is what makes a collision report describe where the sprites
|
||||
have just been moved to rather than where they were (`src/runtime.c`, above the service call),
|
||||
and buying 2% of a frame is not worth changing when a handler fires for every program that
|
||||
already works. Recorded here rather than argued again.
|
||||
|
||||
**Read a benchmark as a gap between two rows of the same run, not as an absolute.** libakgl's
|
||||
`PERFORMANCE.md` records a whole laptop reading 15% high on a later run, including rows nothing
|
||||
had touched.
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
Reference in New Issue
Block a user