247 lines
8.1 KiB
C
247 lines
8.1 KiB
C
|
|
/**
|
||
|
|
* @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;
|
||
|
|
}
|