From d61e352f36b4f5424e9e8de292ed3443d4c9052a Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 09:25:22 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL --- CMakeLists.txt | 21 ++++ MAINTENANCE.md | 47 ++++++++ tests/akgl_backends.c | 199 +++++++++++++++++++++++++++++++++ tests/collision_perf.c | 246 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 513 insertions(+) create mode 100644 tests/collision_perf.c diff --git a/CMakeLists.txt b/CMakeLists.txt index c7aba75..7b3dc80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MAINTENANCE.md b/MAINTENANCE.md index abac7d1..059160c 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -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 diff --git a/tests/akgl_backends.c b/tests/akgl_backends.c index 56db96c..30c7ad2 100644 --- a/tests/akgl_backends.c +++ b/tests/akgl_backends.c @@ -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); diff --git a/tests/collision_perf.c b/tests/collision_perf.c new file mode 100644 index 0000000..319cd47 --- /dev/null +++ b/tests/collision_perf.c @@ -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 + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#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; +}