Measure the library: perf suites, a recorded baseline, and targets
tests/perf.c and tests/perf_render.c drive the hot paths hard enough to time them -- pool acquire and release at both ends of the scan, the registry, the state-to-sprite lookup, the per-actor update and render, the physics sweep, an all-pairs collision sweep, the JSON accessors, path resolution, the drawing primitives, text, asset loading, a screenful of tiles, and a whole frame through akgl_game_update. They are registered like any other suite but carry the `perf` label, so `ctest -L perf` runs only them and `-LE perf` leaves them out. Every measurement is held to a budget at roughly ten times the recorded baseline, enforced only in an optimized build at full scale. Three things had to be got right before the numbers meant anything, and each was wrong first: - No error checking inside the clock. PASS and CATCH call akerr_valid_error_address, which walks AKERR_ARRAY_ERROR -- more work than several of the calls being measured. - Flush the renderer before stopping it. SDL batches, so the first version measured queueing, reported a tilemap frame 250 times faster than it is, and paid the real cost at teardown inside SDL_DestroyTexture. - A drawing benchmark needs a raw-SDL control doing the same pixel work with the same access pattern. With one, akgl_tilemap_draw turns out to cost 0.2% of the frame it appeared to own: 16.26 ms against a control's 16.23 ms for the same 1200 blits. The rasterizer is the frame. PERFORMANCE.md records the baseline, the frame budget it adds up to, and what the numbers say -- including that the string pool's acquire is 64x slower full than empty because it is a megabyte of PATH_MAX buffers, that a handled missing-sprite condition costs nine times the update it replaces, that text rasterizes and throws away a texture on every call, and that 28 MB of the library's BSS is one akgl_Tilemap. TODO.md gains a Performance section: five defects the stress tests found that the unit suites do not reach (a tilemap load leaking five pooled strings, two JSON accessors that turn pool exhaustion into a segfault rather than AKGL_ERR_HEAP, akgl_game_update crashing without akgl_game_init, and its actor update sweep running once per tilemap layer), and eighteen targets with today's number and whether it is met. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
285
tests/benchutil.h
Normal file
285
tests/benchutil.h
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @file benchutil.h
|
||||
* @brief The timing harness behind the perf suites: run it, record it, report it, hold it to a budget.
|
||||
*
|
||||
* A benchmark here is a timed region: bench_start() takes a timestamp,
|
||||
* bench_stop() takes another and divides by the number of units of work that
|
||||
* happened in between. The unit is whatever the measurement is *about* -- one
|
||||
* call, one actor, one tile, one frame -- and it is printed alongside the
|
||||
* number so nobody has to guess what "42 ns" was 42 nanoseconds of.
|
||||
*
|
||||
* Two things make the numbers usable rather than merely present:
|
||||
*
|
||||
* 1. **Best of #AKGL_BENCH_REPETITIONS.** Calling bench_start() again with a
|
||||
* name already in the table folds the new run into the old entry and keeps
|
||||
* the *lowest* ns/op seen. A benchmark loop therefore repeats itself and the
|
||||
* harness reports the run that was interrupted least. The mean is the wrong
|
||||
* statistic here: every source of noise on a shared machine makes a run
|
||||
* slower and none makes it faster.
|
||||
* 2. **The timed region contains no error checking.** `PASS` and `CATCH` both
|
||||
* call `akerr_valid_error_address`, which walks `AKERR_ARRAY_ERROR` looking
|
||||
* for a match -- more work than several of the calls being measured. Use
|
||||
* #BENCH_LOOP, which stashes the context and stops at the first failure, and
|
||||
* hand it to `PASS` after the clock has stopped.
|
||||
*
|
||||
* A budget is a per-op ceiling in nanoseconds, and a benchmark that exceeds it
|
||||
* fails the suite. Budgets are set at roughly ten times the measured baseline:
|
||||
* loose enough that a busy machine does not turn CI red, tight enough that an
|
||||
* algorithmic regression -- a linear scan that becomes quadratic, a per-frame
|
||||
* allocation that becomes a per-actor one -- cannot hide. They are enforced
|
||||
* only in an optimized build at full scale; a coverage build measures the
|
||||
* instrumentation, not the library, and reports without judging.
|
||||
*
|
||||
* Set `AKGL_BENCH_SCALE` in the environment to change how long the suite runs:
|
||||
* 0.1 for a tenth of the iterations, 10 for ten times as many. Below 1.0 the
|
||||
* budgets are not enforced, because a short run is a noisy one.
|
||||
*/
|
||||
|
||||
#ifndef _AKGL_BENCHUTIL_H_
|
||||
#define _AKGL_BENCHUTIL_H_
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <akerror.h>
|
||||
#include <akgl/error.h>
|
||||
|
||||
/** @brief How many benchmarks one suite can record. */
|
||||
#define AKGL_BENCH_MAX_RESULTS 48
|
||||
/** @brief Longest benchmark name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_NAME 56
|
||||
/** @brief Longest unit name kept, including the terminator. */
|
||||
#define AKGL_BENCH_MAX_UNIT 12
|
||||
/** @brief How many times a benchmark is repeated before the best run is reported. */
|
||||
#define AKGL_BENCH_REPETITIONS 5
|
||||
/** @brief Environment variable scaling every iteration count. */
|
||||
#define AKGL_BENCH_SCALE_ENV "AKGL_BENCH_SCALE"
|
||||
|
||||
/** @brief One row of the report: what was measured, how fast, and what it was allowed to cost. */
|
||||
typedef struct akgl_Benchmark {
|
||||
char name[AKGL_BENCH_MAX_NAME]; /**< What was measured. Also the key runs are merged under. */
|
||||
char unit[AKGL_BENCH_MAX_UNIT]; /**< What one op is: "call", "actor", "tile", "frame". */
|
||||
double budget_ns; /**< Per-op ceiling in nanoseconds. 0 reports without judging. */
|
||||
double best_ns; /**< Lowest ns/op seen across every run under this name. */
|
||||
uint64_t ops; /**< Units of work in the run that produced best_ns. */
|
||||
uint64_t elapsed_ns; /**< Wall time of that run. */
|
||||
int runs; /**< How many runs were folded in. */
|
||||
} akgl_Benchmark;
|
||||
|
||||
/** @brief Every benchmark this suite has recorded, in the order it first ran. */
|
||||
static akgl_Benchmark bench_results[AKGL_BENCH_MAX_RESULTS];
|
||||
/** @brief How many entries of #bench_results are in use. */
|
||||
static int bench_result_count = 0;
|
||||
/** @brief The entry bench_stop() will write to, claimed by bench_start(). */
|
||||
static akgl_Benchmark *bench_running = NULL;
|
||||
/** @brief `SDL_GetTicksNS()` at the last bench_start(). */
|
||||
static uint64_t bench_started_ns = 0;
|
||||
|
||||
/**
|
||||
* @brief Multiplier applied to every iteration count, from `AKGL_BENCH_SCALE`.
|
||||
*
|
||||
* Read once and cached. Anything unparseable, negative, or absent gives 1.0.
|
||||
*/
|
||||
static double bench_scale(void)
|
||||
{
|
||||
static double scale = -1.0;
|
||||
const char *env = NULL;
|
||||
|
||||
if ( scale >= 0.0 ) {
|
||||
return scale;
|
||||
}
|
||||
scale = 1.0;
|
||||
env = SDL_getenv(AKGL_BENCH_SCALE_ENV);
|
||||
if ( env != NULL ) {
|
||||
scale = SDL_atof(env);
|
||||
if ( scale <= 0.0 ) {
|
||||
scale = 1.0;
|
||||
}
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Scale a nominal iteration count, never down to zero.
|
||||
* @param count The count the benchmark was written for.
|
||||
* @return @p count times the scale factor, at least 1.
|
||||
*/
|
||||
static int bench_iterations(int count)
|
||||
{
|
||||
int scaled = (int)((double)count * bench_scale());
|
||||
|
||||
if ( scaled < 1 ) {
|
||||
scaled = 1;
|
||||
}
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report whether budgets are being enforced in this run.
|
||||
*
|
||||
* An unoptimized build measures the instrumentation rather than the library,
|
||||
* and a scaled-down run is too short to trust, so both report without failing.
|
||||
*/
|
||||
static bool bench_budgets_enforced(void)
|
||||
{
|
||||
#ifdef __OPTIMIZE__
|
||||
return ( bench_scale() >= 1.0 );
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Start timing, claiming or reusing the table entry named @p name.
|
||||
*
|
||||
* @param name What is being measured. Truncated at #AKGL_BENCH_MAX_NAME.
|
||||
* Reusing a name folds this run into that entry.
|
||||
* @param unit What one op is. Truncated at #AKGL_BENCH_MAX_UNIT.
|
||||
* @param budget_ns Per-op ceiling in nanoseconds; 0 to report only. The value
|
||||
* from the first run under a name wins.
|
||||
*
|
||||
* @note The table is fixed. Past #AKGL_BENCH_MAX_RESULTS entries the run is
|
||||
* dropped with a message rather than overwriting somebody else's row.
|
||||
*/
|
||||
static void bench_start(char *name, char *unit, double budget_ns)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
bench_running = NULL;
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
if ( strncmp(bench_results[i].name, name, AKGL_BENCH_MAX_NAME - 1) == 0 ) {
|
||||
bench_running = &bench_results[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( bench_running == NULL ) {
|
||||
if ( bench_result_count >= AKGL_BENCH_MAX_RESULTS ) {
|
||||
SDL_Log("benchutil: no room for benchmark '%s', raise AKGL_BENCH_MAX_RESULTS", name);
|
||||
return;
|
||||
}
|
||||
bench_running = &bench_results[bench_result_count];
|
||||
bench_result_count += 1;
|
||||
memset(bench_running, 0x00, sizeof(akgl_Benchmark));
|
||||
strncpy(bench_running->name, name, AKGL_BENCH_MAX_NAME - 1);
|
||||
strncpy(bench_running->unit, unit, AKGL_BENCH_MAX_UNIT - 1);
|
||||
bench_running->budget_ns = budget_ns;
|
||||
bench_running->best_ns = -1.0;
|
||||
}
|
||||
bench_started_ns = SDL_GetTicksNS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop timing and keep the run if it beat every previous one.
|
||||
* @param ops Units of work done since bench_start(). Zero is ignored -- a
|
||||
* benchmark that did nothing has no rate to report.
|
||||
*/
|
||||
static void bench_stop(uint64_t ops)
|
||||
{
|
||||
uint64_t elapsed = SDL_GetTicksNS() - bench_started_ns;
|
||||
double per_op = 0.0;
|
||||
|
||||
if ( bench_running == NULL || ops == 0 ) {
|
||||
return;
|
||||
}
|
||||
per_op = (double)elapsed / (double)ops;
|
||||
bench_running->runs += 1;
|
||||
if ( bench_running->best_ns < 0.0 || per_op < bench_running->best_ns ) {
|
||||
bench_running->best_ns = per_op;
|
||||
bench_running->ops = ops;
|
||||
bench_running->elapsed_ns = elapsed;
|
||||
}
|
||||
bench_running = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run @p stmt @p count times, stopping at the first error.
|
||||
*
|
||||
* The timed loop deliberately holds no `PASS` or `CATCH`: their validity check
|
||||
* walks the whole error array and costs more than some of the calls being
|
||||
* measured. Hand @p errvar to `PASS` once the clock has stopped.
|
||||
*
|
||||
* @param errvar Receives the first non-`NULL` context returned, or `NULL`.
|
||||
* @param i Loop variable, declared by the caller.
|
||||
* @param count How many times to run @p stmt.
|
||||
* @param stmt The call under test. Must evaluate to an `akerr_ErrorContext *`.
|
||||
*/
|
||||
#define BENCH_LOOP(errvar, i, count, stmt) \
|
||||
for ( i = 0; i < (count); i++ ) { \
|
||||
errvar = (stmt); \
|
||||
if ( errvar != NULL ) { \
|
||||
break; \
|
||||
} \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the recorded benchmarks as a table and count the ones over budget.
|
||||
*
|
||||
* The table goes to stdout so `ctest --output-on-failure` and a plain run of the
|
||||
* executable both show it. Rates are printed as ops per second, which is the
|
||||
* number a frame budget is actually built from.
|
||||
*
|
||||
* @return How many benchmarks exceeded their budget. Always 0 when
|
||||
* bench_budgets_enforced() is false.
|
||||
*/
|
||||
static int bench_report(void)
|
||||
{
|
||||
int i = 0;
|
||||
int over = 0;
|
||||
bool enforced = bench_budgets_enforced();
|
||||
char *verdict = NULL;
|
||||
|
||||
printf("\n");
|
||||
printf("scale %.2fx, best of %d runs, budgets %s\n",
|
||||
bench_scale(),
|
||||
AKGL_BENCH_REPETITIONS,
|
||||
enforced ? "enforced" : "reported only");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"benchmark", "unit", "ops", "ns/op", "ops/sec", "budget", "verdict");
|
||||
printf("%-54s %-6s %9s %12s %16s %10s %s\n",
|
||||
"------------------------------------------------------",
|
||||
"------", "---------", "------------", "----------------", "----------", "-------");
|
||||
for ( i = 0; i < bench_result_count; i++ ) {
|
||||
verdict = "-";
|
||||
if ( bench_results[i].budget_ns > 0.0 ) {
|
||||
if ( bench_results[i].best_ns > bench_results[i].budget_ns ) {
|
||||
verdict = enforced ? "OVER" : "over";
|
||||
if ( enforced ) {
|
||||
over += 1;
|
||||
}
|
||||
} else {
|
||||
verdict = "ok";
|
||||
}
|
||||
}
|
||||
printf("%-54s %-6s %9llu %12.1f %16.0f %10.0f %s\n",
|
||||
bench_results[i].name,
|
||||
bench_results[i].unit,
|
||||
(unsigned long long)bench_results[i].ops,
|
||||
bench_results[i].best_ns,
|
||||
( bench_results[i].best_ns > 0.0 ) ? (1000000000.0 / bench_results[i].best_ns) : 0.0,
|
||||
bench_results[i].budget_ns,
|
||||
verdict);
|
||||
}
|
||||
printf("\n");
|
||||
fflush(stdout);
|
||||
return over;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Fail the enclosing ATTEMPT block if any benchmark blew its budget.
|
||||
*
|
||||
* The report is printed either way -- a run that fails still has to say what the
|
||||
* numbers were.
|
||||
*/
|
||||
#define BENCH_REPORT_BREAK(e) \
|
||||
{ \
|
||||
int __bench_over = bench_report(); \
|
||||
if ( __bench_over > 0 ) { \
|
||||
FAIL_BREAK( \
|
||||
e, \
|
||||
AKGL_ERR_BEHAVIOR, \
|
||||
"%d benchmark(s) exceeded their budget", \
|
||||
__bench_over); \
|
||||
} \
|
||||
}
|
||||
|
||||
#endif // _AKGL_BENCHUTIL_H_
|
||||
928
tests/perf.c
Normal file
928
tests/perf.c
Normal file
@@ -0,0 +1,928 @@
|
||||
/**
|
||||
* @file perf.c
|
||||
* @brief Stress and timing benchmarks for every subsystem that does not need a renderer.
|
||||
*
|
||||
* These are not correctness tests. Each one drives a hot path -- the pool scans,
|
||||
* the registry, the physics sweep, the per-actor update, the geometry helpers,
|
||||
* the JSON accessors -- hard enough that its per-operation cost is measurable,
|
||||
* and reports what that cost is. The companion suite in `tests/perf_render.c`
|
||||
* covers the paths that need a renderer.
|
||||
*
|
||||
* The pools are the reason this matters. Every acquire is a linear scan of a
|
||||
* fixed array looking for a zero reference count, so cost grows with how full
|
||||
* the pool is, not with how much is being asked for. Several benchmarks below
|
||||
* are therefore run twice: once against an empty pool, which is the number a
|
||||
* casual reading of the code predicts, and once against a nearly full one,
|
||||
* which is the number a game in its fifth minute actually gets.
|
||||
*
|
||||
* Everything here runs headless under the dummy drivers, and every benchmark
|
||||
* rebuilds the state it needs rather than inheriting whatever the previous one
|
||||
* left behind.
|
||||
*
|
||||
* @note SDL's log output is redirected to a sink that discards it. The library
|
||||
* logs on paths this suite hammers -- akgl_actor_initialize logs every
|
||||
* spawn -- and timing a write to a terminal or a CTest capture file
|
||||
* measures the machine's I/O, not libakgl. What the *formatting* costs is
|
||||
* measured deliberately, by the pair of actor-spawn benchmarks.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <string.h>
|
||||
#include <jansson.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/util.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/tilemap.h>
|
||||
|
||||
#include "benchutil.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief How many actors the frame-shaped benchmarks put on the heap. */
|
||||
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
|
||||
/** @brief The JSON document the accessor benchmarks read. */
|
||||
#define BENCH_JSON_FIXTURE "assets/snippets/test_json_helpers.json"
|
||||
/** @brief A path that exists relative to the CTest working directory, for akgl_path_relative. */
|
||||
#define BENCH_PATH_FIXTURE "assets/testcharacter.json"
|
||||
|
||||
/**
|
||||
* @brief Print the library's fixed memory footprint.
|
||||
*
|
||||
* Not a benchmark, but it belongs in the same report: libakgl does not call
|
||||
* `malloc`, so every one of these arrays exists from process start whether the
|
||||
* game uses one slot or all of them, and several of the timings above are
|
||||
* explained entirely by their size. The string pool is a megabyte of `char`
|
||||
* arrays, and an akgl_Tilemap is dominated by 16 layers of 512x512 `int` cells
|
||||
* and 16 tilesets of 65536 offset pairs -- which is why zeroing one costs what
|
||||
* it costs, and why the header warns against putting one on the stack.
|
||||
*/
|
||||
static void bench_report_footprint(void)
|
||||
{
|
||||
size_t total = 0;
|
||||
|
||||
total = sizeof(HEAP_ACTOR) + sizeof(HEAP_SPRITE) + sizeof(HEAP_SPRITESHEET) +
|
||||
sizeof(HEAP_CHARACTER) + sizeof(HEAP_STRING);
|
||||
|
||||
printf("\n");
|
||||
printf("static footprint, fixed at compile time:\n");
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_ACTOR", AKGL_MAX_HEAP_ACTOR, sizeof(akgl_Actor), sizeof(HEAP_ACTOR));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_SPRITE", AKGL_MAX_HEAP_SPRITE, sizeof(akgl_Sprite), sizeof(HEAP_SPRITE));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_SPRITESHEET", AKGL_MAX_HEAP_SPRITESHEET, sizeof(akgl_SpriteSheet), sizeof(HEAP_SPRITESHEET));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_CHARACTER", AKGL_MAX_HEAP_CHARACTER, sizeof(akgl_Character), sizeof(HEAP_CHARACTER));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
"HEAP_STRING", AKGL_MAX_HEAP_STRING, sizeof(akgl_String), sizeof(HEAP_STRING));
|
||||
printf(" %-24s %5s %7s %10zu bytes\n", "pools, total", "", "", total);
|
||||
printf(" %-24s %5d x %7s = %10zu bytes\n",
|
||||
"akgl_Tilemap", 1, "", sizeof(akgl_Tilemap));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
" of which layers", AKGL_TILEMAP_MAX_LAYERS, sizeof(akgl_TilemapLayer),
|
||||
(sizeof(akgl_TilemapLayer) * AKGL_TILEMAP_MAX_LAYERS));
|
||||
printf(" %-24s %5d x %7zu = %10zu bytes\n",
|
||||
" of which tilesets", AKGL_TILEMAP_MAX_TILESETS, sizeof(akgl_Tileset),
|
||||
(sizeof(akgl_Tileset) * AKGL_TILEMAP_MAX_TILESETS));
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Swallow SDL's log output.
|
||||
*
|
||||
* Installed for the whole run. See the note in the file comment: the library
|
||||
* logs on paths this suite calls hundreds of thousands of times, and the cost of
|
||||
* writing that out is not a property of libakgl.
|
||||
*/
|
||||
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Claim a spritesheet slot without loading an image into it.
|
||||
*
|
||||
* akgl_spritesheet_initialize uploads a texture and so needs a renderer, which
|
||||
* this suite deliberately does not have. Nothing here draws, and the only thing
|
||||
* that reads the sheet is akgl_sprite_initialize storing the pointer, so an
|
||||
* empty claimed slot is enough.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_sheet(akgl_SpriteSheet **dest)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
PASS(errctx, akgl_heap_next_spritesheet(dest));
|
||||
memset(*dest, 0x00, sizeof(akgl_SpriteSheet));
|
||||
(*dest)->refcount += 1;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a sprite on a freshly claimed sheet and publish it in the sprite registry.
|
||||
*
|
||||
* @param dest Receives the sprite. Required.
|
||||
* @param name Registry key. Copied at a fixed #AKGL_SPRITE_MAX_NAME_LENGTH
|
||||
* bytes by akgl_sprite_initialize, so it is staged through a buffer
|
||||
* of exactly that size first.
|
||||
* @param speed Nanoseconds one frame is held. 0 makes every akgl_actor_update
|
||||
* advance the animation, which is the expensive path.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_sprite(akgl_Sprite **dest, char *name, uint32_t speed)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_SpriteSheet *sheet = NULL;
|
||||
char namebuf[AKGL_SPRITE_MAX_NAME_LENGTH];
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||||
|
||||
memset(&namebuf, 0x00, sizeof(namebuf));
|
||||
strncpy((char *)&namebuf, name, AKGL_SPRITE_MAX_NAME_LENGTH - 1);
|
||||
|
||||
PASS(errctx, bench_make_sheet(&sheet));
|
||||
PASS(errctx, akgl_heap_next_sprite(dest));
|
||||
PASS(errctx, akgl_sprite_initialize(*dest, (char *)&namebuf, sheet));
|
||||
(*dest)->frames = 3;
|
||||
(*dest)->frameids[0] = 0;
|
||||
(*dest)->frameids[1] = 1;
|
||||
(*dest)->frameids[2] = 2;
|
||||
(*dest)->width = 48;
|
||||
(*dest)->height = 48;
|
||||
(*dest)->speed = speed;
|
||||
(*dest)->loop = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a character with one sprite mapped to the two states these benchmarks use.
|
||||
*
|
||||
* State 0 is what akgl_actor_initialize leaves an actor in and what
|
||||
* akgl_actor_automatic_face leaves it in while it is standing still;
|
||||
* `MOVING_RIGHT | FACE_RIGHT` is what bench_fill_actor_pool puts it in and what
|
||||
* the face logic settles on from there. Mapping both keeps the update path off
|
||||
* the missing-sprite branch, which is measured deliberately elsewhere rather
|
||||
* than by accident here.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_make_character(akgl_Character **dest, char *name, uint32_t spritespeed)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Sprite *sprite = NULL;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||||
|
||||
PASS(errctx, akgl_heap_next_character(dest));
|
||||
PASS(errctx, akgl_character_initialize(*dest, name));
|
||||
(*dest)->sx = 120.0;
|
||||
(*dest)->sy = 120.0;
|
||||
(*dest)->sz = 0.0;
|
||||
(*dest)->ax = 40.0;
|
||||
(*dest)->ay = 40.0;
|
||||
(*dest)->speedtime = 16;
|
||||
|
||||
PASS(errctx, bench_make_sprite(&sprite, name, spritespeed));
|
||||
PASS(errctx, (*dest)->sprite_add(*dest, sprite, 0));
|
||||
PASS(errctx,
|
||||
(*dest)->sprite_add(
|
||||
*dest,
|
||||
sprite,
|
||||
(AKGL_ACTOR_STATE_MOVING_RIGHT | AKGL_ACTOR_STATE_FACE_RIGHT))
|
||||
);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Populate the actor pool with @p count live actors bound to @p basechar.
|
||||
*
|
||||
* The heap and the actor registry are rebuilt first, so this is the state every
|
||||
* frame-shaped benchmark starts from rather than something it inherits.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_fill_actor_pool(akgl_Character *basechar, int count)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int i = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, basechar, AKERR_NULLPOINTER, "basechar");
|
||||
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
|
||||
PASS(errctx, akgl_heap_next_actor(&actor));
|
||||
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
|
||||
PASS(errctx, akgl_actor_set_character(actor, basechar->name));
|
||||
actor->visible = true;
|
||||
actor->layer = 0;
|
||||
actor->x = (float32_t)(i * 13);
|
||||
actor->y = (float32_t)(i * 7);
|
||||
actor->sx = 120.0;
|
||||
actor->sy = 120.0;
|
||||
actor->ax = 40.0;
|
||||
actor->ay = 40.0;
|
||||
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a claim from an empty actor pool against one from a nearly full pool.
|
||||
*
|
||||
* The pool scan stops at the first slot with a zero reference count, so an empty
|
||||
* pool answers from index 0 and a pool with one slot left answers from index
|
||||
* #AKGL_MAX_HEAP_ACTOR - 1. The ratio between these two numbers *is* the cost of
|
||||
* the linear-scan allocator.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_actor_claim(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(500000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
bench_start("heap_next_actor, empty pool", "call", 100.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
for ( i = 0; i < (AKGL_MAX_HEAP_ACTOR - 1); i++ ) {
|
||||
HEAP_ACTOR[i].refcount = 1;
|
||||
}
|
||||
bench_start("heap_next_actor, one slot left", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_next_actor(&actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the string pool, which is the one that hurts.
|
||||
*
|
||||
* Every entry is #AKGL_MAX_STRING_LENGTH bytes, so 256 of them is a megabyte and
|
||||
* the scan for a free slot touches one reference count every 4 KiB -- a cache
|
||||
* miss per candidate. Release then wipes the whole 4 KiB whether the string held
|
||||
* a path or a single character. Three numbers come out of this: the claim on an
|
||||
* empty pool, the claim with one slot left, and the release on its own.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_string(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *str = NULL;
|
||||
int count = bench_iterations(100000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init());
|
||||
|
||||
// Claim and release together: the scratch-buffer idiom the library uses
|
||||
// everywhere, and the only string benchmark whose cost a caller can
|
||||
// actually observe from outside.
|
||||
bench_start("heap string claim + release cycle", "cycle", 500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_string(str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The claim on its own. The reference the claim takes is dropped by hand
|
||||
// rather than through release, so the 4 KiB wipe stays out of this one.
|
||||
bench_start("heap_next_string, empty pool", "call", 100.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
str->refcount = 0;
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The same claim with 255 of the 256 slots taken: a megabyte walked, one
|
||||
// reference count read per 4 KiB page.
|
||||
for ( i = 0; i < (AKGL_MAX_HEAP_STRING - 1); i++ ) {
|
||||
HEAP_STRING[i].refcount = 1;
|
||||
}
|
||||
bench_start("heap_next_string, one slot left", "call", 2500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_string(&str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
str->refcount = 0;
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, akgl_heap_init());
|
||||
|
||||
// Release on its own: one 4 KiB memset per call, whatever the string held.
|
||||
PASS(errctx, akgl_heap_next_string(&str));
|
||||
bench_start("heap_release_string, 4 KiB wipe", "call", 500.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
str->refcount = 1;
|
||||
inner = akgl_heap_release_string(str);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a full reset of every pool.
|
||||
*
|
||||
* akgl_heap_init zeroes all five arrays -- better than a megabyte, dominated by
|
||||
* the string pool. A game pays this once at startup and again on every call to
|
||||
* akgl_game_init; a level transition that calls it per level pays it per level.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_heap_init(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(2000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("heap_init, all five pools", "call", 450000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_heap_init());
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time an actor spawn, with and without the log line the library writes.
|
||||
*
|
||||
* A spawn is a pool claim, a `memset` of the whole actor, a registry insert, and
|
||||
* an `SDL_Log`. The second run raises the log priority so SDL returns before it
|
||||
* formats anything; the gap between the two numbers is what the log line costs
|
||||
* every caller, including the ones that never read it.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_actor_spawn(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
strncpy((char *)&name, "benchspawn", AKGL_ACTOR_MAX_NAME_LENGTH - 1);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
|
||||
bench_start("actor spawn + release, library logging on", "actor", 2100.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_actor(&actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_actor_initialize(actor, (char *)&name);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_actor(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
SDL_SetLogPriorities(SDL_LOG_PRIORITY_CRITICAL);
|
||||
bench_start("actor spawn + release, logging suppressed", "actor", 1700.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_heap_next_actor(&actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_actor_initialize(actor, (char *)&name);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_actor(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
SDL_SetLogPriorities(SDL_LOG_PRIORITY_INFO);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a registry lookup with the actor registry full.
|
||||
*
|
||||
* akgl_actor_set_character is the library's own name-to-pointer lookup: an SDL
|
||||
* property fetch against a registry holding #AKGL_MAX_HEAP_ACTOR entries, plus
|
||||
* the four field copies that follow it. Every actor built from a tilemap object
|
||||
* layer pays it once.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_registry_lookup(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchlookupchar", 0));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_set_character, registry lookup", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_actor_set_character(actor, basechar->name));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the property store a game reads its configuration out of.
|
||||
*
|
||||
* akgl_get_property copies a fixed #AKGL_MAX_STRING_LENGTH bytes out of the
|
||||
* property store regardless of how long the value is, so reading `"0.0"` moves
|
||||
* 4 KiB. akgl_physics_init_arcade makes six of these calls.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_properties(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *value = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, akgl_set_property("bench.property", "a short value"));
|
||||
PASS(errctx, akgl_heap_next_string(&value));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("set_property", "call", 1000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_set_property("bench.property", "a short value"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("get_property, 4 KiB copy", "call", 900.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_property("bench.property", &value, "unset"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(value));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the state-to-sprite lookup that runs twice per actor per frame.
|
||||
*
|
||||
* akgl_character_sprite_get renders the state bitmask to decimal with `SDL_itoa`
|
||||
* and looks the result up as a property string. akgl_actor_update calls it, and
|
||||
* so does akgl_actor_render, so a 64-actor frame makes 128 of these calls before
|
||||
* anything is drawn.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_character_sprite_get(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchspritechar", 0));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("character_sprite_get, state to sprite", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, basechar->sprite_get(basechar, 0, &sprite));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time one actor's logic update, with the animation advancing every call.
|
||||
*
|
||||
* The sprite's frame time is 0, so every update takes the branch that changes
|
||||
* frame -- the expensive path, and the one a fast animation actually takes. An
|
||||
* update is a face recalculation, a sprite lookup, a clock read, and the frame
|
||||
* arithmetic.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_actor_update(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchupdatechar", 0));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, 1));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
AKGL_BITMASK_CLEAR(actor->state);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_update, animation advancing", "actor", 700.0);
|
||||
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// An actor in a state its character has no sprite for is not an error
|
||||
// the library refuses -- akgl_actor_update handles AKERR_KEY and carries
|
||||
// on, and akgl_actor_render logs it and draws nothing. It is a normal
|
||||
// condition on a partly authored character, and it is worth knowing what
|
||||
// one costs, because raising, formatting, and handling a context is far
|
||||
// more work than the update it replaces.
|
||||
AKGL_BITMASK_ADD(actor->state, AKGL_ACTOR_STATE_MOVING_UP);
|
||||
bench_start("actor_update, no sprite for state", "actor", 6200.0);
|
||||
BENCH_LOOP(inner, i, count, actor->updatefunc(actor));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
AKGL_BITMASK_CLEAR(actor->state);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the physics sweep over a full actor pool, and over an empty one.
|
||||
*
|
||||
* akgl_physics_simulate walks all #AKGL_MAX_HEAP_ACTOR slots whether or not
|
||||
* anything is in them, so the empty-pool number is the floor every frame pays.
|
||||
* The full-pool number is one frame of movement for 64 actors: movement logic,
|
||||
* gravity, drag, clamp, and integrate, per actor.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_physics_simulate(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_PhysicsBackend backend;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchphysicschar", 0));
|
||||
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||||
backend.gravity_y = 9.8;
|
||||
backend.drag_x = 0.1;
|
||||
backend.drag_y = 0.1;
|
||||
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("physics_simulate, 64 live actors", "frame", 12500.0);
|
||||
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
|
||||
PASS(errctx, akgl_heap_init_actor());
|
||||
PASS(errctx, akgl_registry_init_actor());
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("physics_simulate, empty pool", "frame", 650.0);
|
||||
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the logic half of a frame: update every actor, then simulate.
|
||||
*
|
||||
* This is what a host's main loop does before it draws anything, and it is the
|
||||
* number to subtract from a 16.6 ms frame to find out what is left for drawing.
|
||||
* Note that it is *not* what akgl_game_update does: that one runs the update
|
||||
* loop once per tilemap layer, which is measured in the render suite.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_logic_frame(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
akgl_PhysicsBackend backend;
|
||||
int count = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_registry_init_properties());
|
||||
PASS(errctx, bench_make_character(&basechar, "benchframechar", 0));
|
||||
memset(&backend, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
PASS(errctx, akgl_physics_init_arcade(&backend));
|
||||
PASS(errctx, bench_fill_actor_pool(basechar, BENCH_ACTOR_COUNT));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("logic frame, 64 actors updated + simulated", "frame", 60000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
for ( j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
|
||||
actor = &HEAP_ACTOR[j];
|
||||
if ( actor->refcount == 0 ) {
|
||||
continue;
|
||||
}
|
||||
inner = actor->updatefunc(actor);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = backend.simulate(&backend, NULL);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the geometry helpers, including the all-pairs sweep a caller has to write.
|
||||
*
|
||||
* akgl_collide_rectangles tests eight corners and returns at the first hit, so
|
||||
* an overlap is cheap and a miss is the full eight. The third benchmark is the
|
||||
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
|
||||
* caller doing collision at all does that every frame.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_geometry(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
RectanglePoints points;
|
||||
SDL_FRect rects[BENCH_ACTOR_COUNT];
|
||||
SDL_FRect overlapping = { .x = 8.0, .y = 8.0, .w = 32.0, .h = 32.0 };
|
||||
SDL_FRect disjoint = { .x = 900.0, .y = 900.0, .w = 32.0, .h = 32.0 };
|
||||
SDL_FRect subject = { .x = 0.0, .y = 0.0, .w = 32.0, .h = 32.0 };
|
||||
bool collide = false;
|
||||
int count = bench_iterations(500000);
|
||||
int sweeps = bench_iterations(2000);
|
||||
int pairs = 0;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
rects[i].x = (float32_t)(i * 24);
|
||||
rects[i].y = (float32_t)(i * 18);
|
||||
rects[i].w = 32.0;
|
||||
rects[i].h = 32.0;
|
||||
}
|
||||
pairs = (BENCH_ACTOR_COUNT * (BENCH_ACTOR_COUNT - 1)) / 2;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("rectangle_points", "call", 100.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_rectangle_points(&points, &subject));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("collide_rectangles, overlapping", "call", 300.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &overlapping, &collide));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("collide_rectangles, disjoint", "call", 600.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collide_rectangles(&subject, &disjoint, &collide));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
|
||||
for ( k = 0; k < sweeps; k++ ) {
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
for ( j = i + 1; j < BENCH_ACTOR_COUNT; j++ ) {
|
||||
inner = akgl_collide_rectangles(&rects[i], &rects[j], &collide);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bench_stop(sweeps);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
printf("all-pairs collision sweep covers %d pairs per sweep\n", pairs);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the pooled-string operations.
|
||||
*
|
||||
* Both of these move #AKGL_MAX_STRING_LENGTH bytes by default -- a copy with a
|
||||
* count of 0 means "all of it" -- so the cost is the same for a filename and for
|
||||
* a single character.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_strings(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *src = NULL;
|
||||
akgl_String *dst = NULL;
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&src));
|
||||
PASS(errctx, akgl_heap_next_string(&dst));
|
||||
PASS(errctx, akgl_string_initialize(src, "assets/testcharacter.json"));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("string_initialize", "call", 350.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_string_initialize(dst, "assets/testcharacter.json"));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("string_copy, full length", "call", 350.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_string_copy(src, dst, 0));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(src));
|
||||
PASS(errctx, akgl_heap_release_string(dst));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time parsing a document and reading values back out of it.
|
||||
*
|
||||
* The parse is what an asset load pays per file. The accessors are what the
|
||||
* tilemap and character loaders pay per field, and every one of them that
|
||||
* returns a string claims and fills a 4 KiB pooled string.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_json(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *path = NULL;
|
||||
akgl_String *value = NULL;
|
||||
json_t *doc = NULL;
|
||||
json_error_t jsonerr;
|
||||
int number = 0;
|
||||
int loads = bench_iterations(2000);
|
||||
int count = bench_iterations(200000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&path));
|
||||
PASS(errctx, akgl_heap_next_string(&value));
|
||||
snprintf((char *)&path->data, AKGL_MAX_STRING_LENGTH, "%s%s", SDL_GetBasePath(), BENCH_JSON_FIXTURE);
|
||||
|
||||
doc = json_load_file((char *)&path->data, 0, &jsonerr);
|
||||
FAIL_ZERO_RETURN(errctx, doc, AKERR_IO, "Unable to load %s: %s", (char *)&path->data, jsonerr.text);
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("json_load_file, small document", "load", 120000.0);
|
||||
for ( i = 0; i < loads; i++ ) {
|
||||
json_t *scratch = json_load_file((char *)&path->data, 0, &jsonerr);
|
||||
if ( scratch == NULL ) {
|
||||
break;
|
||||
}
|
||||
json_decref(scratch);
|
||||
}
|
||||
bench_stop(loads);
|
||||
|
||||
bench_start("get_json_string_value", "call", 450.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_json_string_value(doc, "name", &value));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("get_json_integer_value", "call", 150.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_get_json_integer_value(doc, "count", &number));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
json_decref(doc);
|
||||
PASS(errctx, akgl_heap_release_string(path));
|
||||
PASS(errctx, akgl_heap_release_string(value));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time path resolution, which every asset reference goes through.
|
||||
*
|
||||
* akgl_path_relative calls `realpath(3)`, so this is a syscall and a walk of the
|
||||
* filesystem, per reference, at load time. A tilemap naming twenty tilesets and
|
||||
* sprites pays it twenty times.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_path_relative(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_String *dst = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_heap_next_string(&dst));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("path_relative, realpath on an existing file", "call", 36000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_path_relative(".", BENCH_PATH_FIXTURE, dst));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
PASS(errctx, akgl_heap_release_string(dst));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetLogOutputFunction(bench_discard_log, NULL);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
CATCH(errctx, bench_heap_actor_claim());
|
||||
CATCH(errctx, bench_heap_string());
|
||||
CATCH(errctx, bench_heap_init());
|
||||
CATCH(errctx, bench_actor_spawn());
|
||||
CATCH(errctx, bench_registry_lookup());
|
||||
CATCH(errctx, bench_properties());
|
||||
CATCH(errctx, bench_character_sprite_get());
|
||||
CATCH(errctx, bench_actor_update());
|
||||
CATCH(errctx, bench_physics_simulate());
|
||||
CATCH(errctx, bench_logic_frame());
|
||||
CATCH(errctx, bench_geometry());
|
||||
CATCH(errctx, bench_strings());
|
||||
CATCH(errctx, bench_json());
|
||||
CATCH(errctx, bench_path_relative());
|
||||
|
||||
bench_report_footprint();
|
||||
BENCH_REPORT_BREAK(errctx);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
837
tests/perf_render.c
Normal file
837
tests/perf_render.c
Normal file
@@ -0,0 +1,837 @@
|
||||
/**
|
||||
* @file perf_render.c
|
||||
* @brief Stress and timing benchmarks for the paths that need a renderer.
|
||||
*
|
||||
* The companion to `tests/perf.c`, which covers everything that does not draw.
|
||||
* Everything here runs against a software renderer under the dummy video driver
|
||||
* at #BENCH_SCREEN_W x #BENCH_SCREEN_H -- no display, no window shown, no GPU.
|
||||
*
|
||||
* A software renderer is the right instrument for this even though no shipped
|
||||
* game uses one. What these benchmarks are measuring is the work libakgl does
|
||||
* *per drawn thing* -- how many blits a screenful of tiles turns into, how many
|
||||
* lookups an actor costs before its texture moves, what a line of HUD text
|
||||
* allocates and throws away every frame. A software renderer keeps that work in
|
||||
* the same process where it can be timed, and its per-blit cost is at least
|
||||
* honest about how much pixel traffic was asked for. Read these numbers as
|
||||
* *counts of work*, not as frame rates a real game would see.
|
||||
*
|
||||
* The tilemap benchmarks build their map by hand rather than loading the 2x2
|
||||
* fixture. A 2x2 map measures nothing: the point of the exercise is a full
|
||||
* screen of tiles -- #BENCH_MAP_W x #BENCH_MAP_H cells at #BENCH_TILE_SIZE
|
||||
* pixels, of which the camera covers 40 x 30 -- and the second variant repeats
|
||||
* it with eight tilesets, because akgl_tilemap_draw scans every tileset for
|
||||
* every tile and never stops at the one that matched.
|
||||
*/
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string.h>
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/heap.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/actor.h>
|
||||
#include <akgl/character.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/text.h>
|
||||
#include <akgl/tilemap.h>
|
||||
#include <akgl/staticstring.h>
|
||||
|
||||
#include "benchutil.h"
|
||||
#include "testutil.h"
|
||||
|
||||
/** @brief Width of the offscreen render target every benchmark draws into. */
|
||||
#define BENCH_SCREEN_W 640
|
||||
/** @brief Height of that target. */
|
||||
#define BENCH_SCREEN_H 480
|
||||
/** @brief Edge of one cell in the synthetic map, in pixels. */
|
||||
#define BENCH_TILE_SIZE 16
|
||||
/** @brief Width of the synthetic map, in tiles. Bigger than the camera on purpose. */
|
||||
#define BENCH_MAP_W 128
|
||||
/** @brief Height of the synthetic map, in tiles. */
|
||||
#define BENCH_MAP_H 128
|
||||
/** @brief Tilesets in the many-tileset variant of the draw benchmark. */
|
||||
#define BENCH_MAP_TILESETS 8
|
||||
/** @brief Actors the scene-wide benchmarks put on the heap. */
|
||||
#define BENCH_ACTOR_COUNT AKGL_MAX_HEAP_ACTOR
|
||||
/** @brief The tileset image the synthetic map cuts its tiles from: 768x576, 48x36 tiles. */
|
||||
#define BENCH_TILESET_IMAGE "assets/World_A1.png"
|
||||
/** @brief The font the text benchmarks rasterize with. */
|
||||
#define BENCH_FONT_PATH "assets/akgl_test_mono.ttf"
|
||||
/** @brief Point size for that font. */
|
||||
#define BENCH_FONT_SIZE 16
|
||||
/** @brief A representative line of HUD text: a score readout. */
|
||||
#define BENCH_TEXT "SCORE 000123456"
|
||||
|
||||
/** @brief The font opened once in main and used by the text benchmarks. */
|
||||
static TTF_Font *benchfont = NULL;
|
||||
/** @brief The tileset texture the synthetic map borrows. Destroyed in main. */
|
||||
static SDL_Texture *benchtiles = NULL;
|
||||
|
||||
/**
|
||||
* @brief Stop the clock, but only after the renderer has done what it was asked.
|
||||
*
|
||||
* SDL batches. A `draw_*` call queues a command and returns, and the queue is
|
||||
* not executed until something forces it: a present, a readback, or the
|
||||
* destruction of a texture it refers to. Without a flush inside the timed
|
||||
* region, every drawing benchmark here measures the cost of *queueing* work and
|
||||
* none of the cost of doing it -- and the bill arrives at teardown, where
|
||||
* SDL_DestroyTexture on the tileset spent nearly two minutes executing blits
|
||||
* that six earlier benchmarks had queued and been given credit for not doing.
|
||||
*
|
||||
* So the flush goes inside the measurement: what these benchmarks report is N
|
||||
* calls plus the drawing those N calls asked for, which is the number a caller
|
||||
* actually pays over a frame.
|
||||
*/
|
||||
#define BENCH_FLUSH_STOP(e, ops) \
|
||||
{ \
|
||||
bool __bench_flushed = SDL_FlushRenderer(renderer->sdl_renderer); \
|
||||
bench_stop(ops); \
|
||||
FAIL_ZERO_RETURN(e, __bench_flushed, AKGL_ERR_SDL, "SDL_FlushRenderer: %s", SDL_GetError()); \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Swallow SDL's log output. See the same function in tests/perf.c.
|
||||
*/
|
||||
static void bench_discard_log(void *userdata, int category, SDL_LogPriority priority, const char *message)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a map by hand: one tile layer, @p tilesets tilesets, every cell filled.
|
||||
*
|
||||
* The cells all draw from the *last* tileset, which is the worst case for
|
||||
* akgl_tilemap_draw's inner scan and the reason the many-tileset variant exists.
|
||||
* Global ids are laid out so that tileset k owns [1 + k*tilecount, ...], which
|
||||
* keeps each cell matching exactly one tileset -- the draw loop has no `break`,
|
||||
* so overlapping ranges would blit the same cell more than once and measure
|
||||
* something that cannot happen with a map Tiled produced.
|
||||
*
|
||||
* @param map The tilemap to fill in. Required. Zeroed first.
|
||||
* @param tilesets How many tilesets to declare, all sharing one texture.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_build_map(akgl_Tilemap *map, int tilesets)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int columns = 0;
|
||||
int rows = 0;
|
||||
int tilecount = 0;
|
||||
int lastgid = 0;
|
||||
int i = 0;
|
||||
int cell = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, map, AKERR_NULLPOINTER, "map");
|
||||
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
|
||||
FAIL_NONZERO_RETURN(
|
||||
errctx,
|
||||
((tilesets < 1) || (tilesets > AKGL_TILEMAP_MAX_TILESETS)),
|
||||
AKERR_OUTOFBOUNDS,
|
||||
"%d tilesets is outside 1..%d",
|
||||
tilesets,
|
||||
AKGL_TILEMAP_MAX_TILESETS);
|
||||
|
||||
memset(map, 0x00, sizeof(akgl_Tilemap));
|
||||
columns = benchtiles->w / BENCH_TILE_SIZE;
|
||||
rows = benchtiles->h / BENCH_TILE_SIZE;
|
||||
tilecount = columns * rows;
|
||||
|
||||
map->tilewidth = BENCH_TILE_SIZE;
|
||||
map->tileheight = BENCH_TILE_SIZE;
|
||||
map->width = BENCH_MAP_W;
|
||||
map->height = BENCH_MAP_H;
|
||||
map->numlayers = 1;
|
||||
map->numtilesets = tilesets;
|
||||
|
||||
for ( i = 0; i < tilesets; i++ ) {
|
||||
map->tilesets[i].firstgid = 1 + (i * tilecount);
|
||||
map->tilesets[i].tilecount = tilecount;
|
||||
map->tilesets[i].columns = columns;
|
||||
map->tilesets[i].tilewidth = BENCH_TILE_SIZE;
|
||||
map->tilesets[i].tileheight = BENCH_TILE_SIZE;
|
||||
map->tilesets[i].imagewidth = benchtiles->w;
|
||||
map->tilesets[i].imageheight = benchtiles->h;
|
||||
map->tilesets[i].texture = benchtiles;
|
||||
snprintf((char *)&map->tilesets[i].name, AKGL_TILEMAP_MAX_TILESET_NAME_SIZE, "benchtileset%d", i);
|
||||
PASS(errctx, akgl_tilemap_compute_tileset_offsets(map, i));
|
||||
}
|
||||
|
||||
lastgid = map->tilesets[tilesets - 1].firstgid;
|
||||
map->layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
|
||||
map->layers[0].width = BENCH_MAP_W;
|
||||
map->layers[0].height = BENCH_MAP_H;
|
||||
map->layers[0].visible = true;
|
||||
map->layers[0].opacity = 1.0;
|
||||
for ( i = 0; i < (BENCH_MAP_W * BENCH_MAP_H); i++ ) {
|
||||
cell = i % tilecount;
|
||||
map->layers[0].data[i] = lastgid + cell;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load the test sprite and character, then fill the actor pool with actors wearing it.
|
||||
*
|
||||
* These are the real assets, loaded through the real loaders, so the actors have
|
||||
* a texture-backed sprite and akgl_actor_render takes its full path rather than
|
||||
* bailing out at the missing-sprite branch. Facing is pinned by turning
|
||||
* `movement_controls_face` off: the automatic face logic clears every facing bit
|
||||
* for an actor that is not moving, and the fixture character maps its sprite to
|
||||
* `ALIVE | FACE_LEFT`.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_populate_scene(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akgl_Actor *actor = NULL;
|
||||
char name[AKGL_ACTOR_MAX_NAME_LENGTH];
|
||||
int i = 0;
|
||||
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_registry_init());
|
||||
PASS(errctx, akgl_sprite_load_json("assets/testsprite.json"));
|
||||
PASS(errctx, akgl_sprite_load_json("assets/testsprite2.json"));
|
||||
PASS(errctx, akgl_character_load_json("assets/testcharacter.json"));
|
||||
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
memset(&name, 0x00, sizeof(name));
|
||||
snprintf((char *)&name, AKGL_ACTOR_MAX_NAME_LENGTH, "benchactor%d", i);
|
||||
PASS(errctx, akgl_heap_next_actor(&actor));
|
||||
PASS(errctx, akgl_actor_initialize(actor, (char *)&name));
|
||||
PASS(errctx, akgl_actor_set_character(actor, "testcharacter"));
|
||||
actor->visible = true;
|
||||
actor->layer = 0;
|
||||
actor->movement_controls_face = false;
|
||||
actor->x = (float32_t)((i * 37) % BENCH_SCREEN_W);
|
||||
actor->y = (float32_t)((i * 23) % BENCH_SCREEN_H);
|
||||
actor->curSpriteFrameId = 0;
|
||||
AKGL_BITMASK_ADD(actor->state, (AKGL_ACTOR_STATE_ALIVE | AKGL_ACTOR_STATE_FACE_LEFT));
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the empty frame: clear the target and present it.
|
||||
*
|
||||
* Everything else a frame does is on top of this. It is also the only benchmark
|
||||
* here whose cost is entirely SDL's rather than libakgl's, which makes it the
|
||||
* baseline the rest are worth reading against.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_frame(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("frame_start + frame_end, 640x480", "frame", 230000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = renderer->frame_start(renderer);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = renderer->frame_end(renderer);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the immediate-mode primitives.
|
||||
*
|
||||
* Each of these saves the renderer's draw colour, sets its own, draws, and puts
|
||||
* the colour back, so the fixed cost per call is three SDL round trips whatever
|
||||
* the shape. The shapes are sized like something a game would actually draw: a
|
||||
* full-diagonal line, a dialogue-box-sized rectangle, a 64 pixel radius circle.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_primitives(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
|
||||
SDL_FRect rect = { .x = 32.0, .y = 32.0, .w = 200.0, .h = 150.0 };
|
||||
int count = bench_iterations(100000);
|
||||
int filled = bench_iterations(2000);
|
||||
int circles = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("draw_point", "call", 1400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_point(renderer, 320.0, 240.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_line, screen diagonal", "call", 6200.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_line(renderer, 0.0, 0.0, 639.0, 479.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_rect, 200x150 outline", "call", 5000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_rect(renderer, &rect, red));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_filled_rect, 200x150", "call", 440000.0);
|
||||
BENCH_LOOP(inner, i, filled, akgl_draw_filled_rect(renderer, &rect, red));
|
||||
BENCH_FLUSH_STOP(errctx, filled);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_circle, radius 64", "call", 25000.0);
|
||||
BENCH_LOOP(inner, i, circles, akgl_draw_circle(renderer, 320.0, 240.0, 64.0, red));
|
||||
BENCH_FLUSH_STOP(errctx, circles);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time the framebuffer round trips: save a region, put it back, flood fill.
|
||||
*
|
||||
* These are the expensive ones by construction. A copy reads pixels back off the
|
||||
* render target; a paste uploads a texture, draws it, and destroys it again; and
|
||||
* a flood fill reads back the *entire* target, converts the whole surface to
|
||||
* RGBA32, walks it, uploads a patch, and destroys everything. That is a
|
||||
* megabyte and a half of traffic for one fill at 640x480, whatever the size of
|
||||
* the region that actually changed.
|
||||
*
|
||||
* The fill alternates colours because a fill whose seed pixel already carries
|
||||
* the requested colour returns immediately, and would otherwise measure the
|
||||
* early exit rather than the fill.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_framebuffer(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Surface *saved = NULL;
|
||||
SDL_Rect region = { .x = 64, .y = 64, .w = 64, .h = 64 };
|
||||
SDL_Color red = { 0xff, 0x00, 0x00, 0xff };
|
||||
SDL_Color blue = { 0x00, 0x00, 0xff, 0xff };
|
||||
int count = bench_iterations(5000);
|
||||
int fills = bench_iterations(200);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
PASS(errctx, akgl_draw_copy_region(renderer, ®ion, &saved));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("draw_copy_region, 64x64 readback", "call", 10000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
SDL_Surface *scratch = NULL;
|
||||
inner = akgl_draw_copy_region(renderer, ®ion, &scratch);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
SDL_DestroySurface(scratch);
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_paste_region, 64x64 upload", "call", 55000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_draw_paste_region(renderer, saved, 64.0, 64.0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_flood_fill, full 640x480 target", "call", 20000000.0);
|
||||
for ( i = 0; i < fills; i++ ) {
|
||||
inner = akgl_draw_flood_fill(renderer, 320, 240, ( (i % 2) == 0 ) ? red : blue);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, fills);
|
||||
PASS(errctx, inner);
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
}
|
||||
SDL_DestroySurface(saved);
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time measuring and drawing one line of text.
|
||||
*
|
||||
* akgl_text_rendertextat rasterizes the string, uploads it as a texture, blits
|
||||
* it, and destroys the texture again -- every call, with no cache anywhere. A
|
||||
* HUD with six readouts pays this six times a frame for text that changes once a
|
||||
* second.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_text(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
SDL_Color white = { 0xff, 0xff, 0xff, 0xff };
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int count = bench_iterations(20000);
|
||||
int lines = bench_iterations(5000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("text_measure, 15 characters", "call", 400.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_text_measure(benchfont, BENCH_TEXT, &w, &h));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("text_rendertextat, 15 characters", "call", 130000.0);
|
||||
BENCH_LOOP(inner, i, lines, akgl_text_rendertextat(benchfont, BENCH_TEXT, white, 0, 8, 8));
|
||||
BENCH_FLUSH_STOP(errctx, lines);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time loading assets off disk: a sprite, a character, and a whole map.
|
||||
*
|
||||
* This is startup and level-transition cost, not frame cost, and it is measured
|
||||
* because it is the part of a game the player waits through. The sprite load
|
||||
* after the first reuses the already registered spritesheet, so what is measured
|
||||
* is the JSON path rather than the image decode.
|
||||
*
|
||||
* @note Each character load leaks the SDL property set holding its
|
||||
* state-to-sprite map -- akgl_heap_release_character does not walk it.
|
||||
* That is a known defect rather than something this benchmark introduces;
|
||||
* it is repeated here often enough to be worth saying out loud.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_asset_load(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Sprite *sprite = NULL;
|
||||
akgl_Character *basechar = NULL;
|
||||
int count = bench_iterations(2000);
|
||||
int maps = bench_iterations(50);
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, bench_populate_scene());
|
||||
|
||||
bench_start("sprite_load_json, sheet already loaded", "load", 180000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_sprite_load_json("assets/testsprite.json");
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
sprite = SDL_GetPointerProperty(AKGL_REGISTRY_SPRITE, "testsprite", NULL);
|
||||
if ( sprite == NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_sprite(sprite);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
bench_start("character_load_json, two sprite mappings", "load", 150000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
inner = akgl_character_load_json("assets/testcharacter.json");
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
basechar = SDL_GetPointerProperty(AKGL_REGISTRY_CHARACTER, "testcharacter", NULL);
|
||||
if ( basechar == NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_heap_release_character(basechar);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
bench_start("tilemap_load + release, fixture map", "load", 120000000.0);
|
||||
for ( i = 0; i < maps; i++ ) {
|
||||
// The map's object layer spawns actors into the pool, and neither
|
||||
// akgl_tilemap_release nor anything else gives them back, so a loop
|
||||
// that only loaded and released would exhaust the actor pool on its
|
||||
// 32nd iteration. Clearing the pool is part of what a host does
|
||||
// between levels anyway, and it is a pair of memsets against a map
|
||||
// load that decodes a 768x576 PNG.
|
||||
inner = akgl_heap_init_actor();
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_registry_init_actor();
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
// And the string pool has to be reclaimed by hand, because a map
|
||||
// load leaks five pooled strings that release does not give back.
|
||||
// The 52nd load would find the pool empty, and what happens then is
|
||||
// not an AKGL_ERR_HEAP: akgl_get_json_string_value finishes with
|
||||
// pass-up false, swallows the failed claim, and dereferences the
|
||||
// pointer it never set. Both defects are filed in TODO.md; this loop
|
||||
// works around the first so it never reaches the second.
|
||||
for ( j = 0; j < AKGL_MAX_HEAP_STRING; j++ ) {
|
||||
HEAP_STRING[j].refcount = 0;
|
||||
}
|
||||
inner = akgl_tilemap_load("assets/testmap.tmj", gamemap);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
inner = akgl_tilemap_release(gamemap);
|
||||
if ( inner != NULL ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, maps);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time zeroing an akgl_Tilemap, which is the first thing a map load does.
|
||||
*
|
||||
* akgl_tilemap_load begins with `memset(dest, 0, sizeof(akgl_Tilemap))`, and an
|
||||
* akgl_Tilemap is 16 layers of 512x512 cells plus 16 tilesets of 65536 offset
|
||||
* pairs. The struct is that size whether the map is 2x2 or 512x512, so this is a
|
||||
* fixed toll on every level transition, paid before a single byte of the map
|
||||
* file has been read. Measuring it separately is the only way to tell how much
|
||||
* of the load benchmark above is the map and how much is the container.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_map_zero(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
int count = bench_iterations(200);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("zeroing one akgl_Tilemap", "call", 16000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
memset(gamemap, 0x00, sizeof(akgl_Tilemap));
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time building a tileset's offset table.
|
||||
*
|
||||
* akgl_tilemap_compute_tileset_offsets fills one entry per tile in the image --
|
||||
* 1728 of them for the fixture tileset -- and a map pays it once per tileset at
|
||||
* load time. It is also the function whose table costs 512 KB per tileset,
|
||||
* which is the reason an akgl_Tilemap is as large as it is.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_tileset_offsets(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_compute_tileset_offsets, 1728 tiles", "call", 23000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_compute_tileset_offsets(gamemap, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time drawing a screenful of tiles, with one tileset and with eight.
|
||||
*
|
||||
* The camera covers 40 x 30 cells, so each of these is 1200 blits plus whatever
|
||||
* the tileset scan costs on top. The gap between the two numbers is that scan:
|
||||
* akgl_tilemap_draw walks every tileset for every tile and does not stop at the
|
||||
* one that matched, so the cost is linear in tileset count even though at most
|
||||
* one can ever answer.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_tilemap_draw(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_draw, 40x30 tiles, 1 tileset", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
|
||||
PASS(errctx, bench_build_map(gamemap, BENCH_MAP_TILESETS));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("tilemap_draw, 40x30 tiles, 8 tilesets", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_tilemap_draw(gamemap, camera, 0));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
printf("tilemap_draw covers %d x %d = %d tiles per frame\n",
|
||||
(BENCH_SCREEN_W / BENCH_TILE_SIZE),
|
||||
(BENCH_SCREEN_H / BENCH_TILE_SIZE),
|
||||
((BENCH_SCREEN_W / BENCH_TILE_SIZE) * (BENCH_SCREEN_H / BENCH_TILE_SIZE)));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The control: the same 1200 blits, issued straight to SDL.
|
||||
*
|
||||
* Without this, every drawing number in this file is unreadable -- there is no
|
||||
* way to tell how much of a 16 ms tilemap frame is libakgl deciding what to draw
|
||||
* and how much is the software rasterizer moving pixels. This does the same
|
||||
* amount of pixel work with none of the library in the way: same texture, same
|
||||
* source rectangles, same destinations, no bounds logic, no tileset scan, no
|
||||
* error contexts. Subtract it from the tilemap number and what is left is what
|
||||
* libakgl costs.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_raw_blit_control(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
SDL_FRect src = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
|
||||
SDL_FRect dest = { .x = 0.0, .y = 0.0, .w = BENCH_TILE_SIZE, .h = BENCH_TILE_SIZE };
|
||||
int across = (BENCH_SCREEN_W / BENCH_TILE_SIZE);
|
||||
int down = (BENCH_SCREEN_H / BENCH_TILE_SIZE);
|
||||
int columns = 0;
|
||||
int tilecount = 0;
|
||||
int tile = 0;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int rep = 0;
|
||||
|
||||
FAIL_ZERO_RETURN(errctx, benchtiles, AKERR_NULLPOINTER, "benchtiles");
|
||||
columns = benchtiles->w / BENCH_TILE_SIZE;
|
||||
tilecount = columns * (benchtiles->h / BENCH_TILE_SIZE);
|
||||
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
// One source tile, blitted 1200 times. The floor: 1200 SDL calls and
|
||||
// 1200 x 256 pixels of copying, with the source rectangle staying in
|
||||
// cache the whole way.
|
||||
bench_start("raw SDL blits, one source tile (control)", "frame", 5000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
src.x = 0.0;
|
||||
src.y = 0.0;
|
||||
for ( y = 0; y < down; y++ ) {
|
||||
for ( x = 0; x < across; x++ ) {
|
||||
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
|
||||
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
|
||||
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
|
||||
// The same 1200 blits reading the same scattered source tiles the map
|
||||
// asks for: cell (x, y) of a BENCH_MAP_W-wide map, so consecutive screen
|
||||
// rows read parts of the sheet BENCH_MAP_W tiles apart rather than
|
||||
// walking it in order. This is the honest control for
|
||||
// akgl_tilemap_draw -- same call count, same pixels, same access
|
||||
// pattern, none of libakgl in the path -- and the gap between the two is
|
||||
// what the library's own per-tile work costs.
|
||||
bench_start("raw SDL blits, map order (control)", "frame", 170000000.0);
|
||||
for ( i = 0; i < count; i++ ) {
|
||||
for ( y = 0; y < down; y++ ) {
|
||||
for ( x = 0; x < across; x++ ) {
|
||||
tile = (x + (y * BENCH_MAP_W)) % tilecount;
|
||||
src.x = (float32_t)((tile % columns) * BENCH_TILE_SIZE);
|
||||
src.y = (float32_t)((tile / columns) * BENCH_TILE_SIZE);
|
||||
dest.x = (float32_t)(x * BENCH_TILE_SIZE);
|
||||
dest.y = (float32_t)(y * BENCH_TILE_SIZE);
|
||||
SDL_RenderTexture(renderer->sdl_renderer, benchtiles, &src, &dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time one actor's render, and then the whole scene through the backend.
|
||||
*
|
||||
* A render is two state-to-sprite lookups -- one for the sprite, one inside the
|
||||
* visibility test -- some frame arithmetic, and one blit. The scene benchmark
|
||||
* puts that together with the tilemap: one layer of tiles plus 64 actors, which
|
||||
* is what akgl_render_2d_draw_world does for a modest 2D game.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_scene(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_Actor *actor = NULL;
|
||||
int count = bench_iterations(20000);
|
||||
int frames = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
PASS(errctx, renderer->frame_start(renderer));
|
||||
actor = &HEAP_ACTOR[0];
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("actor_render, on camera", "actor", 31000.0);
|
||||
BENCH_LOOP(inner, i, count, actor->renderfunc(actor));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("draw_world, 1200 tiles + 64 actors", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, frames, renderer->draw_world(renderer, NULL));
|
||||
BENCH_FLUSH_STOP(errctx, frames);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Time a whole frame the way akgl_game_update runs one.
|
||||
*
|
||||
* This is the number a host actually gets, and it is not the sum of the parts.
|
||||
* akgl_game_update's update sweep is nested inside a loop over
|
||||
* #AKGL_TILEMAP_MAX_LAYERS and does not filter by layer, so every live actor is
|
||||
* updated sixteen times per frame rather than once. The gap between this
|
||||
* benchmark and the scene benchmark plus the logic frame in tests/perf.c is that
|
||||
* multiplier, and TODO.md carries it as a defect.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_game_update(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
int count = bench_iterations(30);
|
||||
int i = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, bench_populate_scene());
|
||||
PASS(errctx, bench_build_map(gamemap, 1));
|
||||
PASS(errctx, akgl_physics_init_null(physics));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
bench_start("game_update, full frame", "frame", 170000000.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_game_update(NULL));
|
||||
BENCH_FLUSH_STOP(errctx, count);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||
SDL_SetLogOutputFunction(bench_discard_log, NULL);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akgl_error_init());
|
||||
renderer = &_akgl_renderer;
|
||||
physics = &_akgl_physics;
|
||||
camera = &_akgl_camera;
|
||||
gamemap = &_akgl_gamemap;
|
||||
|
||||
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 the font engine: %s",
|
||||
SDL_GetError());
|
||||
FAIL_ZERO_BREAK(
|
||||
errctx,
|
||||
SDL_CreateWindowAndRenderer(
|
||||
"net/aklabs/libakgl/test_perf_render",
|
||||
BENCH_SCREEN_W,
|
||||
BENCH_SCREEN_H,
|
||||
0,
|
||||
&window,
|
||||
&renderer->sdl_renderer),
|
||||
AKGL_ERR_SDL,
|
||||
"Couldn't create window/renderer: %s",
|
||||
SDL_GetError());
|
||||
CATCH(errctx, akgl_render_bind2d(renderer));
|
||||
|
||||
camera->x = 0.0;
|
||||
camera->y = 0.0;
|
||||
camera->w = BENCH_SCREEN_W;
|
||||
camera->h = BENCH_SCREEN_H;
|
||||
|
||||
// akgl_game_update takes this before it does anything else. Normally
|
||||
// akgl_game_init creates it; this suite brings the library up piece by
|
||||
// piece so it can stay headless, so it creates the mutex itself.
|
||||
game.statelock = SDL_CreateMutex();
|
||||
FAIL_ZERO_BREAK(errctx, game.statelock, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||
|
||||
// And this one is not optional either. akgl_game_updateFPS calls
|
||||
// game.lowfpsfunc through the pointer, unguarded, on every frame under
|
||||
// 30 fps -- which includes the first frame, before there is a frame rate
|
||||
// to compare. A host that brings the library up the way renderer.h
|
||||
// documents for an embedder, akgl_render_bind2d over its own window,
|
||||
// segfaults on its first akgl_game_update. Filed in TODO.md; installing
|
||||
// the default by hand is the workaround.
|
||||
game.lowfpsfunc = &akgl_game_lowfps;
|
||||
|
||||
benchfont = TTF_OpenFont(BENCH_FONT_PATH, BENCH_FONT_SIZE);
|
||||
FAIL_ZERO_BREAK(errctx, benchfont, AKGL_ERR_SDL, "Couldn't open %s: %s", BENCH_FONT_PATH, SDL_GetError());
|
||||
benchtiles = IMG_LoadTexture(renderer->sdl_renderer, BENCH_TILESET_IMAGE);
|
||||
FAIL_ZERO_BREAK(errctx, benchtiles, AKGL_ERR_SDL, "Couldn't load %s: %s", BENCH_TILESET_IMAGE, SDL_GetError());
|
||||
|
||||
CATCH(errctx, akgl_heap_init());
|
||||
CATCH(errctx, akgl_registry_init());
|
||||
CATCH(errctx, akgl_registry_init_properties());
|
||||
|
||||
CATCH(errctx, bench_frame());
|
||||
CATCH(errctx, bench_primitives());
|
||||
CATCH(errctx, bench_framebuffer());
|
||||
CATCH(errctx, bench_text());
|
||||
CATCH(errctx, bench_asset_load());
|
||||
CATCH(errctx, bench_map_zero());
|
||||
CATCH(errctx, bench_tileset_offsets());
|
||||
CATCH(errctx, bench_tilemap_draw());
|
||||
CATCH(errctx, bench_raw_blit_control());
|
||||
CATCH(errctx, bench_scene());
|
||||
CATCH(errctx, bench_game_update());
|
||||
|
||||
BENCH_REPORT_BREAK(errctx);
|
||||
} CLEANUP {
|
||||
if ( benchtiles != NULL ) {
|
||||
SDL_DestroyTexture(benchtiles);
|
||||
}
|
||||
if ( benchfont != NULL ) {
|
||||
TTF_CloseFont(benchfont);
|
||||
}
|
||||
TTF_Quit();
|
||||
SDL_Quit();
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
}
|
||||
Reference in New Issue
Block a user