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:
@@ -549,6 +549,203 @@ static akerr_ErrorContext AKERR_NOIGNORE *test_sprite_from_shape(void)
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run one program and report the collision mask the device computes.
|
||||
*
|
||||
* Straight at the backend rather than through `BUMP(1)`, because what is under
|
||||
* test is the overlap arithmetic and not the accumulate-and-clear the runtime
|
||||
* wraps it in -- that half is already covered by tests/sprite_verbs.c against a
|
||||
* mock. Going through BUMP here would assert both at once and blame the wrong
|
||||
* one when either broke.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *mask_for(const char *source, uint16_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, start_runtime(source));
|
||||
TEST_REQUIRE_STR(OUTPUT, "");
|
||||
PASS(errctx, SPRITES.collisions(&SPRITES, dest));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief What spr_collisions() answers, pinned before anything replaces it.
|
||||
*
|
||||
* **This code had no test at all.** tests/sprite_verbs.c drives the collision
|
||||
* path end to end but through a mock backend, so the real overlap arithmetic
|
||||
* could have changed what BUMP(1) reports for every program in existence and the
|
||||
* suite would still have passed. That is the gap this closes, and it is closed
|
||||
* *before* the arithmetic is touched so there is a before to compare an after
|
||||
* against.
|
||||
*
|
||||
* Two of the cases below are the ones a replacement is most likely to get wrong,
|
||||
* and each says so where it stands.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_collision_pairs(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
uint16_t mask = 0;
|
||||
|
||||
/* Nothing defined: no actor, so nothing to compare and nothing set. */
|
||||
PASS(errctx, mask_for("10 X# = 0\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* Two 24x21 patterns overlapping. Bit n-1 is sprite n, so sprites 1 and 2
|
||||
* are 0x03.
|
||||
*/
|
||||
PASS(errctx, mask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRSAV P#, 2\n"
|
||||
"70 SPRITE 1, 1\n"
|
||||
"80 SPRITE 2, 1\n"
|
||||
"90 MOVSPR 1, 10, 10\n"
|
||||
"100 MOVSPR 2, 20, 20\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x03);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* **Edge to edge is not a collision.** Sprite 1 spans x 10..33 inclusive and
|
||||
* sprite 2 starts at 34, so the test is `ax + aw < bx + 1` -- the strict `<`
|
||||
* in spr_collisions(). A tile-aligned program puts sprites here constantly,
|
||||
* and a replacement that answers "touching" instead of "overlapping" changes
|
||||
* every one of them.
|
||||
*/
|
||||
PASS(errctx, mask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRSAV P#, 2\n"
|
||||
"70 SPRITE 1, 1\n"
|
||||
"80 SPRITE 2, 1\n"
|
||||
"90 MOVSPR 1, 10, 10\n"
|
||||
"100 MOVSPR 2, 34, 10\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
/* A hidden sprite is not in the world, whatever its coordinates say. */
|
||||
PASS(errctx, mask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRSAV P#, 2\n"
|
||||
"70 SPRITE 1, 1\n"
|
||||
"80 SPRITE 2, 0\n"
|
||||
"90 MOVSPR 1, 10, 10\n"
|
||||
"100 MOVSPR 2, 10, 10\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* The x-expand bit doubles the box that collides, not just the one that
|
||||
* draws. Sprite 1 unexpanded ends at x 33 and does not reach sprite 2 at 40;
|
||||
* expanded it spans 48 and does.
|
||||
*/
|
||||
PASS(errctx, mask_for("10 DIM P#(63)\n"
|
||||
"20 FOR I# = 0 TO 62\n"
|
||||
"30 P#(I#) = 255\n"
|
||||
"40 NEXT I#\n"
|
||||
"50 SPRSAV P#, 1\n"
|
||||
"60 SPRSAV P#, 2\n"
|
||||
"70 SPRITE 1, 1, 1, 0, 1, 0\n"
|
||||
"80 SPRITE 2, 1\n"
|
||||
"90 MOVSPR 1, 10, 10\n"
|
||||
"100 MOVSPR 2, 40, 10\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x03);
|
||||
stop_runtime();
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A cross-shaped overlap is a collision, and it is the case to watch.
|
||||
*
|
||||
* A tall thin sprite crossing a short wide one overlaps without either
|
||||
* rectangle containing a corner of the other. libakgl's akgl_collide_rectangles()
|
||||
* is documented as answering "no" here -- a corner-containment test cannot see
|
||||
* it -- which is why src/sprite_akgl.c does the four comparisons itself rather
|
||||
* than calling it.
|
||||
*
|
||||
* So this assertion is the one that fails if collision is ever moved onto a
|
||||
* corner-containment implementation, and it is worth having before rather than
|
||||
* after such a move.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *test_collision_cross_shape(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
uint16_t mask = 0;
|
||||
|
||||
/*
|
||||
* SSHAPE captures a region at whatever size it is asked for, and a sprite
|
||||
* made from one keeps that size -- which is the only way to get two sprites
|
||||
* of different aspect, since a DATA pattern is always 24 by 21.
|
||||
*
|
||||
* Sprite 1 is about 6 wide and 40 tall at (30, 10), so it spans roughly
|
||||
* x 30..36, y 10..50. Sprite 2 is about 40 wide and 6 tall at (10, 30), so
|
||||
* roughly x 10..50, y 30..36. They cross in the middle, and **neither holds
|
||||
* a corner of the other** -- which is the whole point.
|
||||
*
|
||||
* Nothing is drawn into either region first. Collision is by bounding box,
|
||||
* so what a sprite's pixels contain does not enter into it; the only thing
|
||||
* being borrowed from SSHAPE here is the size. That also keeps the margins
|
||||
* wide enough that SSHAPE's inclusive-bound question cannot decide the
|
||||
* answer either way.
|
||||
*/
|
||||
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
|
||||
"20 SSHAPE V$, 0, 0, 6, 40\n"
|
||||
"30 SSHAPE H$, 0, 50, 40, 56\n"
|
||||
"40 SPRSAV V$, 1\n"
|
||||
"50 SPRSAV H$, 2\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SPRITE 2, 1\n"
|
||||
"80 MOVSPR 1, 30, 10\n"
|
||||
"90 MOVSPR 2, 10, 30\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0x03);
|
||||
stop_runtime();
|
||||
|
||||
/*
|
||||
* The two assertions that stop the one above from passing by accident.
|
||||
*
|
||||
* A cross is only a cross if the two sprites really are the long thin shapes
|
||||
* this test believes they are, and nothing above proves that -- two 40x40
|
||||
* blobs at the same coordinates would also report 0x03, for the ordinary
|
||||
* reason, and the test would look like it was doing its job. So move each
|
||||
* one clear along the axis it is *supposed* to be short on. A sprite that
|
||||
* came out the wrong size stays overlapping and fails here.
|
||||
*/
|
||||
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
|
||||
"20 SSHAPE V$, 0, 0, 6, 40\n"
|
||||
"30 SSHAPE H$, 0, 50, 40, 56\n"
|
||||
"40 SPRSAV V$, 1\n"
|
||||
"50 SPRSAV H$, 2\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SPRITE 2, 1\n"
|
||||
"80 MOVSPR 1, 30, 10\n"
|
||||
"90 MOVSPR 2, 10, 60\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
PASS(errctx, mask_for("10 GRAPHIC 1, 1\n"
|
||||
"20 SSHAPE V$, 0, 0, 6, 40\n"
|
||||
"30 SSHAPE H$, 0, 50, 40, 56\n"
|
||||
"40 SPRSAV V$, 1\n"
|
||||
"50 SPRSAV H$, 2\n"
|
||||
"60 SPRITE 1, 1\n"
|
||||
"70 SPRITE 2, 1\n"
|
||||
"80 MOVSPR 1, 60, 10\n"
|
||||
"90 MOVSPR 2, 10, 30\n", &mask));
|
||||
TEST_REQUIRE_INT(mask, 0);
|
||||
stop_runtime();
|
||||
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -615,6 +812,8 @@ int main(void)
|
||||
CATCH(errctx, test_sprite_from_file());
|
||||
CATCH(errctx, test_sprite_from_pattern());
|
||||
CATCH(errctx, test_sprite_from_shape());
|
||||
CATCH(errctx, test_collision_pairs());
|
||||
CATCH(errctx, test_collision_cross_shape());
|
||||
} CLEANUP {
|
||||
if ( font != NULL ) {
|
||||
TTF_CloseFont(font);
|
||||
|
||||
Reference in New Issue
Block a user