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:
2026-08-01 06:45:34 -04:00
parent 3ee8c60491
commit 913834a3af
8 changed files with 379 additions and 60 deletions

View File

@@ -273,7 +273,7 @@ akerr_ErrorContext *akgl_actor_render(akgl_Actor *obj)
dest.y = (obj->y - akgl_camera->y);
}
dest.w = curSprite->width * obj->scale;
dest.h = curSprite->width * obj->scale;
dest.h = curSprite->height * obj->scale;
PASS(errctx, akgl_renderer->draw_texture(akgl_renderer, curSprite->sheet->texture, &src, &dest, 0, NULL, SDL_FLIP_NONE));
SUCCEED_RETURN(errctx);

View File

@@ -643,12 +643,11 @@ akerr_ErrorContext *akgl_game_load(char *fpath)
akerr_ErrorContext *akgl_game_update(akgl_Iterator *opflags)
{
PREPARE_ERROR(errctx);
// No AKGL_ITERATOR_OP_LAYERMASK by default: updating every actor once is
// the whole job, and restricting the sweep to one layer is the caller
// deliberately asking for less.
akgl_Iterator defflags = {
// Was (LAYERMASK | LAYERMASK). Nothing in the loop below reads either
// bit today, so this is a statement of intent rather than a behaviour
// change -- and the reason the loop ignores LAYERMASK is TODO.md
// Performance item 32, which is still open.
.flags = (AKGL_ITERATOR_OP_UPDATE | AKGL_ITERATOR_OP_LAYERMASK),
.flags = AKGL_ITERATOR_OP_UPDATE,
.layerid = 0
};
akgl_Actor *actor = NULL;
@@ -661,22 +660,29 @@ akerr_ErrorContext *akgl_game_update(akgl_Iterator *opflags)
akgl_game_update_fps();
for ( int i = 0; i < AKGL_TILEMAP_MAX_LAYERS; i++ ) {
if ( opflags == &defflags ) {
opflags->layerid = i;
// One sweep, not one per layer. This loop used to sit inside a walk over
// AKGL_TILEMAP_MAX_LAYERS that never compared actor->layer to the layer it
// was on, so every live actor's updatefunc ran sixteen times a frame --
// measured at 68.4 ns per update and 64 actors, 70 us of work to do 4.4 us
// of it. Updating an actor is not a per-layer operation. Drawing is, and
// akgl_render_2d_draw_world walks the layers itself.
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &akgl_heap_actors[j];
if ( actor->refcount == 0 ) {
continue;
}
for ( int j = 0; j < AKGL_MAX_HEAP_ACTOR; j++ ) {
actor = &akgl_heap_actors[j];
if ( actor->refcount == 0 ) {
continue;
}
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
PASS(errctx, akgl_tilemap_scale_actor(akgl_gamemap, actor));
} else {
actor->scale = 1.0;
}
PASS(errctx, actor->updatefunc(actor));
// The flag is honoured now rather than ignored. A caller that sets it
// gets exactly the actors on opflags->layerid, once each.
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) &&
(actor->layer != opflags->layerid) ) {
continue;
}
if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_TILEMAPSCALE) ) {
PASS(errctx, akgl_tilemap_scale_actor(akgl_gamemap, actor));
} else {
actor->scale = 1.0;
}
PASS(errctx, actor->updatefunc(actor));
}
PASS(errctx, akgl_physics->simulate(akgl_physics, NULL));
PASS(errctx, akgl_renderer->draw_world(akgl_renderer, NULL));