Measure collision, and say which numbers moved for what reason
Seven new benchmark rows, budgets set from a measured full-scale run rather than guessed, and the three existing rows the work moved re-recorded from that same run. The all-pairs sweep stays and is relabelled `control:` -- it is the cost a caller paid before the library had a broad phase, measured on the same machine in the same run, which is the only honest way to read a reduction. What the numbers say: - The box fast path is two to seven times cheaper than the general solver, and the disjoint case is cheaper still because the proxies' bounds reject it before any shape arithmetic runs. 9 to 20 ns is what a tile game actually pays. - The grid beats the tree by 2.6x on the same population, which is the argument for the default measured here rather than cited from another engine. The tree also rebuilds on every move and that is not in its row, so a moving scene is worse than 2.6x. - A grid `move` that changes nothing is 11.5 ns. That is the incremental claim in one number. Two rows moved on a backend with **no collision world attached**, so no collision code runs in either, and the commit says so rather than letting the feature take credit: - akgl_Actor grew from 415 to 464 bytes -- a 40-byte shape, an override flag and a proxy pointer. The step sweeps the whole pool, so that is about 3 KB more working set per frame. The empty-pool row is unchanged at 58.7 ns, which is what identifies the cost as per live actor rather than per slot. - Reaching the collision check through CATCH cost more than the check. PASS and CATCH call akerr_valid_error_address, which walks AKERR_ARRAY_ERROR, so an early-returning function is not free when it is reached through one. The no-collision path now routes around the machinery entirely, which took the 64-actor sweep from 2,018.7 ns back to 1,588.5. AGENTS.md records that lesson for writing benchmarks; this is the same thing in production code. The remainder is the struct, and it is paid whether or not a game uses collision. Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
135
tests/perf.c
135
tests/perf.c
@@ -40,6 +40,7 @@
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/collision.h>
|
||||
#include <akgl/util.h>
|
||||
#include <akgl/json_helpers.h>
|
||||
#include <akgl/tilemap.h>
|
||||
@@ -625,7 +626,7 @@ static akerr_ErrorContext *bench_physics_simulate(void)
|
||||
|
||||
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_start("physics_simulate, 64 live actors", "frame", 16000.0);
|
||||
BENCH_LOOP(inner, i, count, backend.simulate(&backend, NULL));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
@@ -705,6 +706,135 @@ static akerr_ErrorContext *bench_logic_frame(void)
|
||||
* broad phase the library does not provide: 64 actors is 2016 pairs, and a
|
||||
* caller doing collision at all does that every frame.
|
||||
*/
|
||||
/** @brief Counts nothing; the query cost is what is being measured. */
|
||||
static akerr_ErrorContext *bench_collision_visit(akgl_CollisionProxy *proxy, void *data)
|
||||
{
|
||||
(void)proxy;
|
||||
(void)data;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** @brief A map with a floor, for the tile-collision row. */
|
||||
static akgl_Tilemap bench_collision_map;
|
||||
|
||||
/**
|
||||
* @brief Collision: the narrowphase, the broad phase, and a whole sweep.
|
||||
*
|
||||
* The all-pairs row in bench_geometry stays where it is and is the control for
|
||||
* these: it is the cost a caller paid before the library had a broad phase, run
|
||||
* on the same machine in the same run, which is the only way to read a
|
||||
* reduction honestly.
|
||||
*/
|
||||
static akerr_ErrorContext *bench_collision(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
akerr_ErrorContext *inner = NULL;
|
||||
akgl_CollisionWorld world;
|
||||
akgl_CollisionShape boxshape;
|
||||
akgl_CollisionShape circleshape;
|
||||
akgl_CollisionProxy a;
|
||||
akgl_CollisionProxy b;
|
||||
akgl_CollisionProxy *proxies[BENCH_ACTOR_COUNT];
|
||||
akgl_Contact contact;
|
||||
SDL_FRect body = { .x = -8.0, .y = -8.0, .w = 16.0, .h = 16.0 };
|
||||
SDL_FRect area = { .x = 0.0, .y = 0.0, .w = 48.0, .h = 48.0 };
|
||||
bool hit = false;
|
||||
int count = bench_iterations(500000);
|
||||
int sweeps = bench_iterations(20000);
|
||||
int i = 0;
|
||||
int x = 0;
|
||||
int rep = 0;
|
||||
|
||||
PASS(errctx, akgl_collision_shape_box(&boxshape, &body, 0.0));
|
||||
PASS(errctx, akgl_collision_shape_circle(&circleshape, 0.0, 0.0, 8.0, 0.0));
|
||||
|
||||
for ( rep = 0; rep < AKGL_BENCH_REPETITIONS; rep++ ) {
|
||||
PASS(errctx, akgl_heap_init());
|
||||
PASS(errctx, akgl_collision_world_init(&world, "grid", 16.0, 16.0));
|
||||
|
||||
PASS(errctx, akgl_collision_proxy_initialize(&a, NULL, &boxshape, 0.0, 0.0, 0.0));
|
||||
PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &boxshape, 4.0, 0.0, 0.0));
|
||||
bench_start("narrowphase box/box, overlapping", "call", 210.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collision_test(&a, &b, world.flags, &contact, &hit));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &boxshape, 900.0, 900.0, 0.0));
|
||||
bench_start("narrowphase box/box, disjoint", "call", 90.0);
|
||||
BENCH_LOOP(inner, i, count, akgl_collision_test(&a, &b, world.flags, &contact, &hit));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
PASS(errctx, akgl_collision_proxy_initialize(&a, NULL, &circleshape, 0.0, 0.0, 0.0));
|
||||
PASS(errctx, akgl_collision_proxy_initialize(&b, NULL, &circleshape, 4.0, 0.0, 0.0));
|
||||
bench_start("narrowphase circle/circle, overlapping", "call", 680.0);
|
||||
BENCH_LOOP(inner, i, bench_iterations(100000),
|
||||
akgl_collision_test(&a, &b, world.flags, &contact, &hit));
|
||||
bench_stop(bench_iterations(100000));
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The broad phase, with a realistic population spread over the map.
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
PASS(errctx, akgl_heap_next_collision_proxy(&proxies[i]));
|
||||
PASS(errctx, akgl_collision_proxy_initialize(proxies[i], NULL, &boxshape,
|
||||
(float32_t)((i % 20) * 24),
|
||||
(float32_t)((i / 20) * 24), 0.0));
|
||||
PASS(errctx, world.partitioner.insert(&world.partitioner, proxies[i]));
|
||||
}
|
||||
|
||||
bench_start("grid move, unchanged cells", "actor", 120.0);
|
||||
BENCH_LOOP(inner, i, count, world.partitioner.move(&world.partitioner, proxies[0]));
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("grid query, 64 actors", "query", 440.0);
|
||||
BENCH_LOOP(inner, i, sweeps,
|
||||
world.partitioner.query(&world.partitioner, &area,
|
||||
AKGL_COLLISION_LAYER_ALL, &bench_collision_visit, NULL));
|
||||
bench_stop(sweeps);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// The same population through the tree, so the two are comparable.
|
||||
PASS(errctx, akgl_collision_world_init(&world, "bsp", 16.0, 16.0));
|
||||
for ( i = 0; i < BENCH_ACTOR_COUNT; i++ ) {
|
||||
PASS(errctx, world.partitioner.insert(&world.partitioner, proxies[i]));
|
||||
}
|
||||
bench_start("bsp query, 64 actors", "query", 1140.0);
|
||||
BENCH_LOOP(inner, i, sweeps,
|
||||
world.partitioner.query(&world.partitioner, &area,
|
||||
AKGL_COLLISION_LAYER_ALL, &bench_collision_visit, NULL));
|
||||
bench_stop(sweeps);
|
||||
PASS(errctx, inner);
|
||||
|
||||
// One actor against a floor of tiles, which is the case a platformer
|
||||
// runs every frame for every actor.
|
||||
memset(&bench_collision_map, 0x00, sizeof(akgl_Tilemap));
|
||||
bench_collision_map.tilewidth = 16;
|
||||
bench_collision_map.tileheight = 16;
|
||||
bench_collision_map.width = 128;
|
||||
bench_collision_map.height = 32;
|
||||
bench_collision_map.numlayers = 1;
|
||||
bench_collision_map.layers[0].type = AKGL_TILEMAP_LAYER_TYPE_TILES;
|
||||
for ( x = 0; x < bench_collision_map.width; x++ ) {
|
||||
bench_collision_map.layers[0].data[(20 * bench_collision_map.width) + x] = 1;
|
||||
}
|
||||
bench_collision_map.collidablelayers = 1u;
|
||||
PASS(errctx, akgl_collision_world_init(&world, "grid", 16.0, 16.0));
|
||||
PASS(errctx, akgl_collision_bind_tilemap(&world, &bench_collision_map));
|
||||
|
||||
area.x = 100.0;
|
||||
area.y = 310.0;
|
||||
area.w = 16.0;
|
||||
area.h = 16.0;
|
||||
bench_start("tile query, actor on a floor", "query", 160.0);
|
||||
BENCH_LOOP(inner, i, sweeps, akgl_collision_box_blocked(&world, &area,
|
||||
AKGL_COLLISION_LAYER_ALL, &hit));
|
||||
bench_stop(sweeps);
|
||||
PASS(errctx, inner);
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *bench_geometry(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -747,7 +877,7 @@ static akerr_ErrorContext *bench_geometry(void)
|
||||
bench_stop(count);
|
||||
PASS(errctx, inner);
|
||||
|
||||
bench_start("all-pairs collision sweep, 64 actors", "sweep", 1200000.0);
|
||||
bench_start("control: 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++ ) {
|
||||
@@ -916,6 +1046,7 @@ int main(void)
|
||||
CATCH(errctx, bench_physics_simulate());
|
||||
CATCH(errctx, bench_logic_frame());
|
||||
CATCH(errctx, bench_geometry());
|
||||
CATCH(errctx, bench_collision());
|
||||
CATCH(errctx, bench_strings());
|
||||
CATCH(errctx, bench_json());
|
||||
CATCH(errctx, bench_path_relative());
|
||||
|
||||
Reference in New Issue
Block a user