f06c53110da31f1001613f7ac557a6b90f50bdef
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
f06c53110d
|
Answer sprite collision through libakgl's narrowphase
`spr_collisions()` computed axis-aligned overlaps itself, because at libakgl 0.7.0 there was nothing to delegate to: `akgl_collide_rectangles()` has a documented corner-containment defect and the physics backend's `collide` slot raised "Not implemented". 0.8.0 brought a real narrowphase, and this moves onto it. The mask is bit-identical and every test from the previous commit passes **unmodified**, which was the gate this stage had to clear -- including the two that were written to be hard to satisfy. Edge-to-edge is still not a collision, so a tile-aligned program is unaffected. The cross-shaped overlap is still reported, which is the one that could have regressed: it is the case `akgl_collide_rectangles()` gets wrong and the reason the hand-written loop existed, and `akgl_collision_test()`'s box path gets it right. What it buys is the contact -- a normal, a penetration depth and a point -- which four comparisons cannot produce. Nothing consumes it yet; that is the next commit. It is here now because the mask and the contact come out of the same test, and computing them in two places would be two things to keep in step. **The first attempt was ten times slower and the benchmark caught it.** Syncing all eight proxies and running the narrowphase on all twenty-eight pairs measured 984 ns a scan against 96 ns for the loop it replaced -- 21% of a frame at 256 scans a frame -- to produce a mask that was bit-identical and a contact that was thrown away. Two changes fixed it, and both are what a broad phase *is* rather than workarounds for a slow library: - **Reject on the bounding boxes first.** The four comparisons that were always here now decide which pairs are worth an exact answer. The narrowphase still decides the bit -- the box test only says "maybe", which will matter the moment a shape is not the whole frame. - **Do not sync a proxy that has not moved.** The scan runs at the top of every interpreter step and a sprite moves at most once in that time, so almost every sync would rewrite a proxy with what it already holds. Compared against the last synced rectangle rather than flagged by the verbs, because a host game can move a BASIC sprite through the actor registry and a flag would miss that. Measured after: 54.9 ns at eight sprites spread out, which is *faster* than the 96.3 ns it replaced -- boxes are now built eight times a scan instead of fifty-six. The number that matters is the new benchmark row for the arrangement `examples/breakout/sprites/breakout.bas` actually has, which is 211.8 ns, or 4.5% of a frame. That game reaches it because two of its eight sprites are the screen -- a captured HUD strip and a captured play field -- so the field's box covers everything and those pairs can never be rejected. Roughly double the old cost, for contacts. Recorded in MAINTENANCE.md with the two synthetic extremes either side of it as a bracket. The eight proxies are claimed once at init and held, so exhaustion of the pool shared with an embedding host is an initialization failure that names the pool rather than a collision scan refusing halfway through somebody's game. The shape is built before the proxy is spawned from it and the acquire sits adjacent to the initialize, which are two traps libakgl hit itself and documents. `tests/akgl_backends.c` now tears the sprite backend down between cases. It never did, and got away with it while init claimed nothing; eight proxies apiece across twenty cases is a hundred and sixty against a pool of a hundred and twenty-eight. A host releases what it took, and so does the harness. Both games run forty seconds headless with no error line. 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL |
|||
|
d61e352f36
|
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 |