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

43
TODO.md
View File

@@ -975,13 +975,15 @@ without coming here first. Ordered by blast radius.
`src/character.c:124`.
26. **`akgl_actor_render` computes a sprite's drawn height from its width.**
`src/actor.c:276` sets `dest.h = curSprite->width * obj->scale` where it
should read `curSprite->height`. Every actor is therefore drawn square, and
a non-square sprite is stretched or squashed. Invisible in the current
assets because they are square; it will not stay that way.
**Fixed in 0.5.0**: `dest.h` takes `curSprite->height`. Every actor was
drawn square, so a non-square sprite was stretched or squashed -- invisible
in the fixtures because they are all square.
Fix: one word. Touches `src/actor.c:276`. Worth a test that renders a
deliberately non-square sprite and asserts on the destination rectangle.
`tests/actor.c` renders a 48x24 sprite through a backend whose
`draw_texture` records the rectangle it is handed rather than drawing it,
and asserts the destination is 48x24 and 96x48 at scale 2. That recording
backend is also the first coverage `akgl_actor_render` has had at all --
every other test in the file stubs `renderfunc` out.
### Found while closing the akbasic API gaps
@@ -1095,18 +1097,25 @@ Ordered by blast radius. Numbering continues the **Defects** list above.
workaround and no longer has to.
32. **`akgl_game_update` runs the actor update sweep once per tilemap layer.**
`src/game.c:617` loops `i` over `AKGL_TILEMAP_MAX_LAYERS` and the actor
sweep nested inside it never compares `actor->layer` to `i`, so every live
actor's `updatefunc` runs **16 times per frame**. Measured: 68.4 ns per
update, 64 actors, so 70 µs of work to do 4.4 µs of work.
**Fixed in 0.5.0.** The sweep is hoisted out of the layer loop -- updating
an actor is not a per-layer operation, and `akgl_render_2d_draw_world`
already walks the layers for the half that is.
It hides behind the rasterizer today (a 640x480 software frame is 16 ms) and
it will not hide behind a GPU backend, where a frame is nearer 2 ms and this
is 3.5% of it. Fix: either filter by layer like `akgl_render_2d_draw_world`
does, or hoist the update sweep out of the layer loop entirely — updating an
actor is not a per-layer operation. The second is almost certainly right,
and it is the one that needs a test asserting `updatefunc` runs exactly once
per actor per `akgl_game_update`.
`AKGL_ITERATOR_OP_LAYERMASK` is honoured rather than ignored now, so a
caller that genuinely wants one layer can ask for it and gets each of those
actors once. It is no longer in the default flag set: every live actor once
is the job, and restricting the sweep is the caller asking for less.
`tests/game.c` counts calls into a stub `updatefunc` and asserts exactly one
per live actor per frame, two over two frames, and the layer mask selecting
only its own layer. Against the old code it reports 16.
Counting is the assertion on purpose. 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. The timing evidence is the
gap between the `akgl_game_update` and `draw_world` rows of the same
benchmark run: 92 us before, and noise in both directions after. See
`PERFORMANCE.md`.
33. **`akgl_heap_release_character` leaks its `state_sprites` property set.**
**Fixed in 0.5.0**; see item 21, which is the same defect. The