/** * @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; }