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:
@@ -167,6 +167,14 @@ rows are raw `SDL_RenderTexture` loops with no libakgl in the path.
|
||||
| `draw_world`, 1200 tiles + 64 actors | frame | 16,484,493.6 | 61 |
|
||||
| `akgl_game_update`, full frame | frame | 16,576,902.2 | 60 |
|
||||
|
||||
The `akgl_game_update` row is from before the sixteen-times-per-actor defect was
|
||||
fixed in 0.5.0; see below. It sat about 92 µs above `draw_world` in the same
|
||||
run, and now sits level with it. The absolute figures in this table have not
|
||||
been re-taken -- a later run on this machine read every row about 15% high,
|
||||
including rows nothing has touched, so the table is a record of one machine
|
||||
state and the honest measurement of that fix is the *gap* between two rows of
|
||||
the same run, not a new absolute.
|
||||
|
||||
## The frame budget
|
||||
|
||||
At 60 fps a frame is 16.67 ms. Here is where it goes for a 640x480 game with a
|
||||
@@ -247,16 +255,30 @@ The design conclusion is not "make errors cheaper". It is that a *routine*
|
||||
condition should not be reported as an error. A character that has no sprite for
|
||||
a state should answer that question with a boolean.
|
||||
|
||||
### `akgl_game_update` updates every actor sixteen times
|
||||
### `akgl_game_update` updated every actor sixteen times — fixed in 0.5.0
|
||||
|
||||
`src/game.c:617` loops over `AKGL_TILEMAP_MAX_LAYERS`, and the actor sweep
|
||||
nested inside it does not filter by layer. Every live actor's `updatefunc` runs
|
||||
**16 times per frame**. At 68.4 ns per update and 64 actors that is 70 µs of
|
||||
work to do 4.4 µs of work.
|
||||
`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.
|
||||
Every live actor's `updatefunc` ran **16 times per frame**: at 68.4 ns per
|
||||
update and 64 actors, 70 µs of work to do 4.4 µs of it — and, more to the
|
||||
point, every piece of a game's own per-frame actor logic running sixteen times.
|
||||
|
||||
It is invisible in the frame total here because the tilemap blits are three
|
||||
orders of magnitude larger. On a GPU backend, where the frame might be 2 ms, it
|
||||
is 3.5% of the frame doing nothing. Filed in `TODO.md`.
|
||||
The sweep is hoisted out of the layer loop. Updating an actor is not a per-layer
|
||||
operation; drawing is, and `akgl_render_2d_draw_world` walks the layers itself.
|
||||
`AKGL_ITERATOR_OP_LAYERMASK` is honoured now rather than ignored, so a caller
|
||||
that genuinely wants one layer can still ask for it and gets each of those
|
||||
actors once.
|
||||
|
||||
**How it was measured.** `tests/game.c` counts calls into a stub `updatefunc`
|
||||
and asserts exactly one per live actor per `akgl_game_update`; against the old
|
||||
code it reports 16. The timing side is the gap between the `akgl_game_update`
|
||||
and `draw_world` rows of the *same* benchmark run: 92 µs before, and across
|
||||
three runs afterwards -414 µs, -376 µs and +19 µs — noise in both directions.
|
||||
|
||||
It was invisible in the frame total because the tilemap blits are three orders
|
||||
of magnitude larger, which is exactly why it needed a counting test rather than
|
||||
a stopwatch. On a GPU backend, where a frame might be 2 ms, it was 3.5% of the
|
||||
frame doing nothing.
|
||||
|
||||
### Text has no cache at all
|
||||
|
||||
|
||||
Reference in New Issue
Block a user