Draw actors at their sprite's height, and update each one once a frame
Closes Defects item 26 and Performance item 32. akgl_actor_render set dest.h from curSprite->width, so every actor was drawn square and a non-square sprite was stretched or squashed. Invisible in the fixtures because they are all square, so tests/actor.c gets a 48x24 sprite and a render backend whose draw_texture records the rectangle it is handed instead of drawing it. That recording backend is the first coverage akgl_actor_render has had at all -- every other test in the file stubs renderfunc out. akgl_game_update looped over AKGL_TILEMAP_MAX_LAYERS with the actor sweep nested inside it and never compared an actor's layer to the layer it was on, so every live actor's updatefunc ran sixteen times a frame. The sweep is hoisted out: updating an actor is not a per-layer operation, and akgl_render_2d_draw_world already walks the layers for the half that is. AKGL_ITERATOR_OP_LAYERMASK is honoured rather than ignored now, so a caller who wants one layer can still ask, and gets each of those actors once. Counting is the assertion, deliberately. The defect was invisible in the frame total because the tilemap blits are three orders of magnitude larger, so a timing test would have measured the rasterizer. tests/game.c counts calls into a stub updatefunc and reports 16 against the old code. PERFORMANCE.md records the timing side as what it honestly is: the gap between the akgl_game_update and draw_world rows of the same run, 92 us before and noise in both directions after. The absolute table is not re-taken -- a later run on this machine read every row about 15% high, including rows nothing has touched. 25/25 pass, reindent --check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
tests/game.c
139
tests/game.c
@@ -21,6 +21,9 @@
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/sprite.h>
|
||||
#include <akgl/staticstring.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/physics.h>
|
||||
#include <akgl/iterator.h>
|
||||
|
||||
#include "testutil.h"
|
||||
|
||||
@@ -510,6 +513,141 @@ akerr_ErrorContext *test_game_updateFPS(void)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Counts akgl_game_update calls into each actor's updatefunc, by actor index. */
|
||||
static int updatecounts[AKGL_MAX_HEAP_ACTOR];
|
||||
|
||||
/** @brief updatefunc stub: record that this actor was updated. */
|
||||
static akerr_ErrorContext *counting_updatefunc(akgl_Actor *obj)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
int i = 0;
|
||||
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
if ( &akgl_heap_actors[i] == obj ) {
|
||||
updatecounts[i] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Physics backend stub: akgl_game_update calls simulate, and it must not matter here. */
|
||||
static akerr_ErrorContext *stub_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Render backend stub: akgl_game_update calls draw_world; drawing is not under test. */
|
||||
static akerr_ErrorContext *stub_draw_world(akgl_RenderBackend *self, akgl_Iterator *opflags)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief akgl_game_update must call each live actor's updatefunc exactly once.
|
||||
*
|
||||
* The sweep used to sit inside a walk over AKGL_TILEMAP_MAX_LAYERS and never
|
||||
* compared actor->layer to the layer it was on, so every live actor updated
|
||||
* sixteen times a frame -- 70 microseconds of work to do 4.4 of it, and every
|
||||
* bit of per-frame actor logic running sixteen times over. It hid behind a
|
||||
* software rasterizer and would not have hidden behind a GPU backend.
|
||||
*
|
||||
* Counting is the assertion. A timing test would measure the machine.
|
||||
*/
|
||||
akerr_ErrorContext *test_game_update_visits_each_actor_once(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
akgl_PhysicsBackend stubphysics;
|
||||
akgl_RenderBackend stubrenderer;
|
||||
akgl_Iterator opflags;
|
||||
akgl_Actor *actors[3] = { NULL, NULL, NULL };
|
||||
int layers[3] = { 0, 1, 1 };
|
||||
int i = 0;
|
||||
int live = 0;
|
||||
|
||||
ATTEMPT {
|
||||
set_game_identity();
|
||||
akgl_game.statelock = SDL_CreateMutex();
|
||||
FAIL_ZERO_BREAK(e, akgl_game.statelock, AKGL_ERR_SDL, "unable to create the state mutex");
|
||||
akgl_game.lowfpsfunc = &akgl_game_lowfps;
|
||||
|
||||
CATCH(e, akgl_heap_init());
|
||||
CATCH(e, akgl_registry_init_actor());
|
||||
|
||||
memset(&stubphysics, 0x00, sizeof(akgl_PhysicsBackend));
|
||||
memset(&stubrenderer, 0x00, sizeof(akgl_RenderBackend));
|
||||
stubphysics.simulate = &stub_simulate;
|
||||
stubrenderer.draw_world = &stub_draw_world;
|
||||
akgl_physics = &stubphysics;
|
||||
akgl_renderer = &stubrenderer;
|
||||
akgl_gamemap = &akgl_default_gamemap;
|
||||
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
char name[32];
|
||||
snprintf((char *)&name, sizeof(name), "sweepactor%d", i);
|
||||
CATCH(e, akgl_heap_next_actor(&actors[i]));
|
||||
CATCH(e, akgl_actor_initialize(actors[i], (char *)&name));
|
||||
actors[i]->updatefunc = &counting_updatefunc;
|
||||
actors[i]->layer = layers[i];
|
||||
}
|
||||
|
||||
// The default sweep: every live actor, once.
|
||||
memset(&updatecounts, 0x00, sizeof(updatecounts));
|
||||
TEST_EXPECT_OK(e, akgl_game_update(NULL), "one default game update");
|
||||
live = 0;
|
||||
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||
if ( akgl_heap_actors[i].refcount == 0 ) {
|
||||
TEST_ASSERT(e, updatecounts[i] == 0,
|
||||
"a free actor slot %d was updated %d times", i, updatecounts[i]);
|
||||
continue;
|
||||
}
|
||||
live += 1;
|
||||
TEST_ASSERT(e, updatecounts[i] == 1,
|
||||
"actor %d updated %d times in one frame, expected 1",
|
||||
i, updatecounts[i]);
|
||||
}
|
||||
TEST_ASSERT(e, live == 3, "%d live actors, expected 3", live);
|
||||
|
||||
// Two frames means two updates, not thirty-two.
|
||||
memset(&updatecounts, 0x00, sizeof(updatecounts));
|
||||
TEST_EXPECT_OK(e, akgl_game_update(NULL), "the second game update");
|
||||
TEST_EXPECT_OK(e, akgl_game_update(NULL), "the third game update");
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
TEST_ASSERT(e, updatecounts[i] == 2,
|
||||
"actor %d updated %d times over two frames, expected 2",
|
||||
i, updatecounts[i]);
|
||||
}
|
||||
|
||||
// AKGL_ITERATOR_OP_LAYERMASK now means what it says: only layer 1.
|
||||
memset(&updatecounts, 0x00, sizeof(updatecounts));
|
||||
AKGL_BITMASK_CLEAR(opflags.flags);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_UPDATE);
|
||||
AKGL_BITMASK_ADD(opflags.flags, AKGL_ITERATOR_OP_LAYERMASK);
|
||||
opflags.layerid = 1;
|
||||
TEST_EXPECT_OK(e, akgl_game_update(&opflags), "a layer-masked game update");
|
||||
TEST_ASSERT(e, updatecounts[0] == 0,
|
||||
"the layer 0 actor updated %d times under a layer 1 mask", updatecounts[0]);
|
||||
TEST_ASSERT(e, updatecounts[1] == 1,
|
||||
"the first layer 1 actor updated %d times, expected 1", updatecounts[1]);
|
||||
TEST_ASSERT(e, updatecounts[2] == 1,
|
||||
"the second layer 1 actor updated %d times, expected 1", updatecounts[2]);
|
||||
} CLEANUP {
|
||||
for ( i = 0; i < 3; i++ ) {
|
||||
if ( actors[i] != NULL ) {
|
||||
IGNORE(akgl_heap_release_actor(actors[i]));
|
||||
}
|
||||
}
|
||||
if ( akgl_game.statelock != NULL ) {
|
||||
SDL_DestroyMutex(akgl_game.statelock);
|
||||
akgl_game.statelock = NULL;
|
||||
}
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -535,6 +673,7 @@ int main(void)
|
||||
CATCH(errctx, test_game_state_lock_budget());
|
||||
CATCH(errctx, test_game_updateFPS());
|
||||
CATCH(errctx, test_game_updateFPS_without_a_lowfps_handler());
|
||||
CATCH(errctx, test_game_update_visits_each_actor_once());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
Reference in New Issue
Block a user