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:
2026-08-02 09:25:22 -04:00
parent 1fb808f480
commit d61e352f36
4 changed files with 513 additions and 0 deletions

View File

@@ -354,6 +354,27 @@ if(AKBASIC_WITH_AKGL)
TIMEOUT 60
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy")
# What the collision scan costs against what a frame costs. Labelled `perf` so
# `ctest -LE perf` leaves it out of the ordinary run: it is the only test here
# that deliberately spends wall-clock time, and the numbers are meaningless in
# the Debug trees anyway -- see the file's own header.
add_executable(akbasic_test_collision_perf tests/collision_perf.c)
target_compile_options(akbasic_test_collision_perf PRIVATE -Wall -Wextra)
target_link_libraries(akbasic_test_collision_perf PRIVATE akbasic_akgl)
# libakgl's benchmark harness, borrowed by include path rather than copied, for
# the same reason the fixture font is: a second copy is a fork. SYSTEM because
# benchutil.h is a header of statics and -Wunused-function would fire on every
# helper this suite happens not to call.
target_include_directories(akbasic_test_collision_perf SYSTEM PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests")
target_compile_definitions(akbasic_test_collision_perf PRIVATE
AKBASIC_TEST_FONT="${CMAKE_CURRENT_SOURCE_DIR}/deps/libakgl/tests/assets/akgl_test_mono.ttf")
_add_test(NAME collision_perf COMMAND akbasic_test_collision_perf)
_set_tests_properties(collision_perf PROPERTIES
LABELS perf
TIMEOUT 300
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software")
# The frontend suite. Separate from the one above because it tests the *host*
# rather than the adaptors -- it is the only test that links akbasic_frontend,
# and the only one that opens a window and pumps events. It uses the

View File

@@ -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

View File

@@ -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);

246
tests/collision_perf.c Normal file
View File

@@ -0,0 +1,246 @@
/**
* @file collision_perf.c
* @brief What the sprite collision scan costs, and what a frame costs beside it.
*
* This exists to settle one question with a number rather than an argument:
* `akbasic_collision_service()` runs at the top of every interpreter *step*
* (`src/runtime.c`), and the akgl frontend takes
* #AKBASIC_FRONTEND_STEPS_PER_FRAME steps per rendered frame -- so a busy
* program scans for collisions up to 256 times a frame, nearly always over
* sprites that have not moved since the last scan. Whether that is worth
* changing depends entirely on how the scan compares to the frame it sits
* inside, and nobody had measured either.
*
* **The control row is the point.** A scan measured on its own is a number with
* nothing to divide it by. `frame` renders what a real frame renders -- the
* whole text grid and the sprites -- so the scan rows can be read as a fraction
* of it. libakgl's own PERFORMANCE.md makes the same argument at length, and
* records getting it wrong the first time by measuring queued work rather than
* finished work.
*
* The harness is libakgl's, borrowed by include path rather than copied: it is
* the house convention for a benchmark in these repositories, and a second copy
* would be a fork.
*
* Labelled `perf` in CTest so `ctest -LE perf` skips it. Numbers taken at
* `-O0`, which is what both checked-in build trees are, are not worth reading;
* configure a RelWithDebInfo tree and run `AKGL_BENCH_SCALE=10 ctest -L perf`.
* Budgets are report-only unless the build is optimised, which `benchutil.h`
* enforces for exactly that reason.
*/
#include <string.h>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <akerror.h>
#include <akgl/error.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/renderer.h>
#include <akgl/text.h>
#include <akbasic/akgl.h>
#include <akbasic/error.h>
#include <akbasic/frontend.h>
#include <akbasic/runtime.h>
#include <akbasic/sink.h>
#include <akbasic/sprite.h>
#include "benchutil.h"
/** @brief The window a real program gets from the standalone frontend. */
#define TARGET_W 800
#define TARGET_H 600
static akbasic_Runtime RUNTIME;
static akbasic_TextSink SINK;
static akbasic_AkglSink SINKSTATE;
static akbasic_GraphicsBackend GRAPHICS;
static akbasic_AkglGraphics GRAPHICSSTATE;
static akbasic_SpriteBackend SPRITES;
static akbasic_AkglSprites SPRITESSTATE;
static TTF_Font *font = NULL;
/**
* @brief Stand a runtime up on the real akgl devices and run @p source to completion.
*
* The akgl sink rather than the stdio one, because the control row has to render
* the text layer and that is the only sink that draws.
*/
static akerr_ErrorContext AKERR_NOIGNORE *load(const char *source)
{
PREPARE_ERROR(errctx);
PASS(errctx, akbasic_sink_init_akgl(&SINK, &SINKSTATE, akgl_renderer, font,
TARGET_W, TARGET_H));
PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer));
PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, akgl_renderer, &GRAPHICSSTATE));
PASS(errctx, akbasic_runtime_set_devices(&RUNTIME, &GRAPHICS, NULL, NULL, &SPRITES));
PASS(errctx, akbasic_runtime_load(&RUNTIME, source));
PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUN));
PASS(errctx, akbasic_runtime_run(&RUNTIME, 0));
SUCCEED_RETURN(errctx);
}
/**
* @brief A program defining @p n sprites, all overlapping at the origin.
*
* Overlapping rather than scattered on purpose: it is the worst case for the
* pair loop, since every pair that passes the visibility guards goes on to the
* four comparisons and sets two bits. A scattered arrangement measures the
* early-out instead, and the early-out is not the thing anybody is worried about.
*/
static void sprite_program(char *dest, size_t size, int n)
{
char line[128];
int i = 0;
snprintf(dest, size,
"10 DIM P#(63)\n"
"20 FOR I# = 0 TO 62\n"
"30 P#(I#) = 255\n"
"40 NEXT I#\n");
for ( i = 1; i <= n; i++ ) {
snprintf(line, sizeof(line),
"%d SPRSAV P#, %d\n%d SPRITE %d, 1\n%d MOVSPR %d, 20, 20\n",
100 + (i * 10), i,
101 + (i * 10), i,
102 + (i * 10), i);
strncat(dest, line, size - strlen(dest) - 1);
}
}
/** @brief Time the collision scan with @p n sprites live. */
static akerr_ErrorContext AKERR_NOIGNORE *bench_scan(int n)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
char source[4096];
char name[64];
uint16_t mask = 0;
int iterations = bench_iterations(200000);
int i = 0;
memset(source, 0, sizeof(source));
sprite_program(source, sizeof(source), n);
PASS(errctx, load(source));
snprintf(name, sizeof(name), "spr_collisions, %d sprites", n);
bench_start(name, "call", 0.0);
BENCH_LOOP(inner, i, iterations, SPRITES.collisions(&SPRITES, &mask));
bench_stop((uint64_t)iterations);
PASS(errctx, inner);
SUCCEED_RETURN(errctx);
}
/**
* @brief What one rendered frame costs: the text layer, the sprites and a present.
*
* The denominator. This is the same sequence and the same order
* `akbasic_frontend_akgl_pump()` runs, so the ratio the scan rows are read
* against is a real one rather than an analogy.
*/
static akerr_ErrorContext AKERR_NOIGNORE *bench_frame(void)
{
PREPARE_ERROR(errctx);
akerr_ErrorContext *inner = NULL;
char source[4096];
int iterations = bench_iterations(300);
int i = 0;
memset(source, 0, sizeof(source));
sprite_program(source, sizeof(source), 8);
/* A screenful of text, so the sink renders what a real program's sink renders. */
strncat(source,
"900 FOR R# = 0 TO 30\n"
"910 CHAR 1, 0, R#, \"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG 0123456789\"\n"
"920 NEXT R#\n",
sizeof(source) - strlen(source) - 1);
PASS(errctx, load(source));
bench_start("one rendered frame, 8 sprites + text grid", "frame", 0.0);
for ( i = 0; i < iterations; i++ ) {
inner = akbasic_sink_akgl_render(&SINK);
if ( inner != NULL ) {
break;
}
inner = akbasic_sprite_akgl_render(&SPRITES);
if ( inner != NULL ) {
break;
}
if ( !SDL_RenderPresent(akgl_renderer->sdl_renderer) ) {
break;
}
}
bench_stop((uint64_t)iterations);
PASS(errctx, inner);
SUCCEED_RETURN(errctx);
}
int main(void)
{
PREPARE_ERROR(errctx);
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
ATTEMPT {
CATCH(errctx, akgl_error_init());
akgl_renderer = &akgl_default_renderer;
FAIL_ZERO_BREAK(errctx, SDL_Init(SDL_INIT_VIDEO), AKGL_ERR_SDL,
"Couldn't initialize SDL: %s", SDL_GetError());
FAIL_ZERO_BREAK(errctx, TTF_Init(), AKGL_ERR_SDL,
"Couldn't initialize SDL_ttf: %s", SDL_GetError());
FAIL_ZERO_BREAK(errctx,
SDL_CreateWindowAndRenderer("net/aklabs/akbasic/collision_perf",
TARGET_W, TARGET_H, 0,
&akgl_window, &akgl_renderer->sdl_renderer),
AKGL_ERR_SDL, "Couldn't create window/renderer: %s", SDL_GetError());
CATCH(errctx, akgl_render_2d_bind(akgl_renderer));
CATCH(errctx, akgl_heap_init());
CATCH(errctx, akgl_registry_init());
akgl_camera = &akgl_default_camera;
akgl_camera->x = 0.0f;
akgl_camera->y = 0.0f;
akgl_camera->w = (float)TARGET_W;
akgl_camera->h = (float)TARGET_H;
font = TTF_OpenFont(AKBASIC_TEST_FONT, 16);
FAIL_ZERO_BREAK(errctx, font, AKGL_ERR_SDL,
"Couldn't open %s: %s", AKBASIC_TEST_FONT, SDL_GetError());
CATCH(errctx, bench_scan(0));
CATCH(errctx, bench_scan(2));
CATCH(errctx, bench_scan(4));
CATCH(errctx, bench_scan(8));
CATCH(errctx, bench_frame());
BENCH_REPORT_BREAK(errctx);
} CLEANUP {
if ( font != NULL ) {
TTF_CloseFont(font);
}
TTF_Quit();
if ( akgl_window != NULL ) {
SDL_DestroyWindow(akgl_window);
akgl_window = NULL;
}
SDL_Quit();
} PROCESS(errctx) {
} HANDLE_DEFAULT(errctx) {
LOG_ERROR_WITH_MESSAGE(errctx, "collision benchmark failed");
return 1;
/*
* FINISH_NORETURN rather than FINISH, matching src/main.c and
* tests/akgl_backends.c: FINISH expands a `return __err_context` that an
* int-returning function cannot compile even where the branch is dead.
*/
} FINISH_NORETURN(errctx);
return 0;
}